diff --git a/app/controllers/print_templates_controller.rb b/app/controllers/print_templates_controller.rb index 28bbd48..0bca81d 100644 --- a/app/controllers/print_templates_controller.rb +++ b/app/controllers/print_templates_controller.rb @@ -1,17 +1,14 @@ class PrintTemplatesController < ApplicationController - layout ->{ @project ? 'base' : 'admin' } + layout -> { @project ? 'base' : 'admin' } self.main_menu = false before_action :set_trackers, only: [:new, :create, :edit, :update] - before_action :find_print_template, only: [:edit, :update, :destroy] + before_action :find_print_template, only: [:edit, :update, :destroy, :show] before_action :require_admin, except: [:show, :fields_for_tracker] - # TODO: make this work with Rails.ajax - # before_action :authorize_view_print_templates, only: [:show, :fields_for_tracker] - def index - @print_templates = PrintTemplate.includes(:tracker).all + @print_templates = PrintTemplate.includes(:tracker).all.order(name: :asc) end def new @@ -28,7 +25,6 @@ def create end def edit - # @print_template is set by before_action end def update @@ -48,71 +44,116 @@ def destroy end def show - @print_template = PrintTemplate.find(params[:id]) + # Define the keys that need to be parsed as JSON + json_keys = ['schemas'] + + # Parse specified JSON strings to nested JSON + parsed_json = @print_template.attributes.each_with_object({}) do |(key, value), hash| + hash[key] = if json_keys.include?(key) && value.is_a?(String) + begin + JSON.parse(value) + rescue JSON::ParserError + value + end + else + value + end + end + + fields_data = generate_fields_data(@print_template.tracker) + + # Merge fields data into parsed_json + merged_json = parsed_json.merge(fields_data) + respond_to do |format| - format.json { render json: @print_template } - format.js + format.json { render json: merged_json } end end def fields_for_tracker - @tracker = Tracker.find(params[:tracker_id]) - - # Define core fields - @core_fields = { - 'standard#author.name' => ['text', 'field_author'], - 'standard#status.name' => ['text', 'field_status'], - 'standard#priority.name' => ['text', 'field_priority'], - 'standard#assigned_to.name' => ['text', 'field_assigned_to'], - 'standard#category.name' => ['text', 'field_category'], - 'standard#fixed_version.name' => ['text', 'field_fixed_version'], - 'standard#subject' => ['text', 'field_subject'], - 'standard#description' => ['text', 'field_description'], - 'standard#start_date' => ['date', 'field_start_date'], - 'standard#due_date' => ['date', 'field_due_date'], - 'standard#done_ratio' => ['text', 'field_done_ratio'], - 'standard#estimated_hours' => ['text', 'field_estimated_hours'], - 'standard#total_estimated_hours' => ['text', 'field_total_estimated_hours'], - 'standard#spent_hours' => ['text', 'label_spent_time'], - 'standard#total_spent_hours' => ['text', 'label_total_spent_time'], - 'standard#created_on' => ['date', 'field_created_on'], - 'standard#updated_on' => ['date', 'field_updated_on'], - 'standard#closed_on' => ['date', 'field_closed_on'], + tracker = Tracker.find(params[:tracker_id]) + fields_data = generate_fields_data(tracker) + render json: fields_data + end + + private + + def generate_fields_data(tracker) + # Define the fields that are available for the print template + core_fields = { + 'author.name' => ['string', l(:field_author)], + 'status.name' => ['string', l(:field_status)], + 'priority.name' => ['string', l(:field_priority)], + 'assigned_to.name' => ['string', l(:field_assigned_to)], + 'category.name' => ['string', l(:field_category)], + 'fixed_version.name' => ['string', l(:field_fixed_version)], + 'subject' => ['string', l(:field_subject)], + 'description' => ['text', l(:field_description)], + 'start_date' => ['date', l(:field_start_date)], + 'due_date' => ['date', l(:field_due_date)], + 'done_ratio' => ['float', l(:field_done_ratio)], + 'estimated_hours' => ['float', l(:field_estimated_hours)], + 'total_estimated_hours' => ['float', l(:field_total_estimated_hours)], + 'spent_hours' => ['float', l(:label_spent_time)], + 'total_spent_hours' => ['float', l(:label_total_spent_time)], + 'created_on' => ['date', l(:field_created_on)], + 'updated_on' => ['date', l(:field_updated_on)], + 'closed_on' => ['date', l(:field_closed_on)], }.map { |field, attributes| create_field_hash(field, *attributes) } - # Define custom fields with their names directly - @custom_fields = @tracker.custom_fields.map do |cf| - field_identifier = "custom#issue_custom_field_values_#{cf.id}" + # Custom fields + custom_fields = tracker.custom_fields.map do |cf| + field_key = "cf_#{cf.id}" field_format = cf.field_format - field_name = cf.name + field_label = cf.name - create_field_hash(field_identifier, field_format, field_name) + create_field_hash(field_key, field_format, field_label) end - # Define special fields with localization keys - @special_fields = { - 'special#issue_map' => ['image', 'field_issue_map'], - 'special#issue_url' => ['qrcode', 'field_issue_url'] + # Special fields + special_fields = { + 'issue_map' => ['map', l(:field_issue_map)], + 'issue_url' => ['link', l(:field_issue_url)], }.map { |field, attributes| create_field_hash(field, *attributes) } - # Sorting - @core_fields.sort_by! { |field| field[:name].downcase } - @custom_fields.sort_by! { |field| field[:name].downcase } - @special_fields.sort_by! { |field| field[:name].downcase } + # Field formats + format_list = { + 'bool' => ['boolean', 'Boolean'], + 'date' => ['date', 'Date'], + # 'attachment' => ['file', 'File'], + 'float' => ['float', 'Float'], + 'int' => ['integer', 'Integer'], + # 'enumeration' => ['enumeration', 'Key/value list'], + # 'link' => ['link', 'Link'], + # 'list' => ['list', 'List'], + # 'text' => ['text', 'Long text'], + 'string' => ['string', 'Text'], + # 'user' => ['user', 'User'], + # 'version' => ['version', 'Version'], + }.map { |field, attributes| create_field_hash(field, *attributes) } - respond_to do |format| - format.js - end + # Return the fields data + { + 'fieldKeyOptions': [{ + 'label': l(:label_core_fields), + 'options': core_fields.sort_by! { |field| field[:value].downcase } + }, { + 'label': l(:label_custom_fields), + 'options': custom_fields.sort_by! { |field| field[:value].downcase } + }, { + 'label': l(:label_special_fields), + 'options': special_fields.sort_by! { |field| field[:value].downcase } + }], + 'fieldFormatOptions': format_list + } end - private - def create_field_hash(field, format, name_or_key) name = I18n.exists?(name_or_key) ? I18n.t(name_or_key) : name_or_key { - name: name, - identifier: field, + label: name, + value: field, format: format } end @@ -126,7 +167,7 @@ def find_print_template end def print_template_params - params.require(:print_template).permit(:name, :schemas, :inputs, :basepdf, :tracker_id) + params.require(:print_template).permit(:name, :schemas, :basepdf, :tracker_id, :context) end def require_admin diff --git a/app/controllers/print_templates_pdfme_controller.rb b/app/controllers/print_templates_pdfme_controller.rb index d1f1d97..5ff43bb 100644 --- a/app/controllers/print_templates_pdfme_controller.rb +++ b/app/controllers/print_templates_pdfme_controller.rb @@ -23,7 +23,7 @@ def designer @csrf_token = form_authenticity_token end - def form + def viewer issue_id = params[:issue_id] api_key = User.current.api_key @@ -55,13 +55,6 @@ def form @csrf_token = form_authenticity_token end - # Future actions for viewer, generator, etc. - # def viewer - # end - - # def generator - # end - private def authorize_view_print_templates diff --git a/app/models/print_template.rb b/app/models/print_template.rb index aaac6db..6516930 100644 --- a/app/models/print_template.rb +++ b/app/models/print_template.rb @@ -5,4 +5,9 @@ class PrintTemplate < ActiveRecord::Base validates :name, uniqueness: { scope: :tracker_id } validates :tracker_id, presence: true + + # The context of the print template must be one of the following values. + CONTEXT_OPTIONS = %w(issue issues project).freeze + + validates :context, presence: true, inclusion: { in: CONTEXT_OPTIONS } end diff --git a/app/views/print_templates/_form.html.erb b/app/views/print_templates/_form.html.erb index 811fccb..435f5e3 100644 --- a/app/views/print_templates/_form.html.erb +++ b/app/views/print_templates/_form.html.erb @@ -1,15 +1,28 @@

<%= f.text_field :name, required: true, size: 50 %>

+ +

+ <%= f.select :context, + options_for_select([ + [ l(:label_print_template_context_option_issue), 'issue' ], + [ l(:label_print_template_context_option_issues), 'issues', { disabled: true } ], + [ l(:label_print_template_context_option_project), 'project', { disabled: true } ] + ], + @print_template.context || 'issue'), + { required: true } %> +

+ +

<%= f.select :tracker_id, - options_from_collection_for_select(@trackers, :id, :name, @print_template.tracker_id || @trackers.first.id), - { required: true }, - { data: { url: url_for(controller: 'print_templates', action: 'fields_for_tracker', format: :js) } } %> + options_from_collection_for_select(@trackers, :id, :name, @print_template.tracker_id || @trackers.first.id), + { required: true }, + { data: { url: url_for(controller: 'print_templates', action: 'fields_for_tracker', format: :json) } } %>

<%= content_tag(:label, l(:label_print_template_basepdf), for: 'pdf-upload') %> - + <%= link_to l(:link_print_template_basepdf_reset), '#', id: 'use-blank-pdf', style: ('display: none;' unless @print_template.basepdf.present?) %>

@@ -24,24 +37,12 @@ <%= f.hidden_field :schemas, value: @print_template.schemas, id: 'print_template_schemas' %> <%= f.hidden_field :basepdf, value: @print_template.basepdf, id: 'print_template_basepdf' %> - <%= f.hidden_field :inputs, value: @print_template.inputs, id: 'print_template_inputs' %>
<% else %>

<%= l(:label_no_print_template_available) %> diff --git a/app/views/print_templates/_list.html.erb b/app/views/print_templates/_list.html.erb index 72e464a..056dd1e 100644 --- a/app/views/print_templates/_list.html.erb +++ b/app/views/print_templates/_list.html.erb @@ -4,6 +4,7 @@ <%= l :label_print_template_name %> <%= l :label_tracker %> + <%= l :label_print_template_context %> <%= l :label_print_template_basepdf %> diff --git a/app/views/print_templates/_print_template.html.erb b/app/views/print_templates/_print_template.html.erb index 9e41016..0edb324 100644 --- a/app/views/print_templates/_print_template.html.erb +++ b/app/views/print_templates/_print_template.html.erb @@ -1,6 +1,18 @@ +<% def localized_context(context) %> + <% case context %> + <% when 'issue' %> + <%= l(:label_print_template_context_option_issue) %> + <% when 'issues' %> + <%= l(:label_print_template_context_option_issues) %> + <% else %> + <%= context %> + <% end %> +<% end %> + - <%= link_to print_template.name, edit_print_template_path(print_template) %> - <%= textilizable print_template.tracker.name %> + <%= link_to print_template.name, edit_print_template_path(print_template) %> + <%= textilizable print_template.tracker.name %> + <%= textilizable localized_context(print_template.context) %> <% if print_template.basepdf.present? %> diff --git a/app/views/print_templates/fields_for_tracker.js.erb b/app/views/print_templates/fields_for_tracker.js.erb deleted file mode 100644 index 6e9efc9..0000000 --- a/app/views/print_templates/fields_for_tracker.js.erb +++ /dev/null @@ -1,33 +0,0 @@ -var fieldsSelect = document.getElementById('tracker-fields'); -fieldsSelect.innerHTML = ''; - -// Helper function to create an optgroup -function createOptgroup(label, fields) { - var optgroup = document.createElement('optgroup'); - optgroup.label = label; - - fields.forEach(function(field) { - var option = document.createElement('option'); - option.value = field.identifier; - option.text = field.name; - option.setAttribute('data-format', field.format); - optgroup.appendChild(option); - }); - - return optgroup; -} - -// Add core fields optgroup -<% if @core_fields.any? %> -fieldsSelect.appendChild(createOptgroup('Core Fields', <%= @core_fields.to_json.html_safe %>)); -<% end %> - -// Add custom fields optgroup -<% if @custom_fields.any? %> -fieldsSelect.appendChild(createOptgroup('Custom Fields', <%= @custom_fields.to_json.html_safe %>)); -<% end %> - -// Add special fields optgroup -<% if @special_fields.any? %> -fieldsSelect.appendChild(createOptgroup('Special Fields', <%= @special_fields.to_json.html_safe %>)); -<% end %> diff --git a/app/views/print_templates_pdfme/designer.html.erb b/app/views/print_templates_pdfme/designer.html.erb index ef3addf..ea75e30 100644 --- a/app/views/print_templates_pdfme/designer.html.erb +++ b/app/views/print_templates_pdfme/designer.html.erb @@ -8,11 +8,11 @@

-
Reload page to start PDFme Designer...
+
<%= l(:reload_page_for_designer) %>
<%= javascript_include_tag 'pdfme-designer', plugin: :redmine_print_templates %> diff --git a/app/views/print_templates_pdfme/form.html.erb b/app/views/print_templates_pdfme/form.html.erb deleted file mode 100644 index 7290421..0000000 --- a/app/views/print_templates_pdfme/form.html.erb +++ /dev/null @@ -1,18 +0,0 @@ - - - - <% plugin_name = Redmine::Plugin.find(:redmine_print_templates).name rescue 'Print Templates' %> - <%= "#{plugin_name} - Form" %> - - <%= stylesheet_link_tag('print_pdfme', plugin: 'redmine_print_templates') %> - - -
Reload page to start PDFme Form...
- <%= javascript_include_tag 'pdfme-form', plugin: :redmine_print_templates %> - - - diff --git a/app/views/print_templates_pdfme/viewer.html.erb b/app/views/print_templates_pdfme/viewer.html.erb new file mode 100644 index 0000000..4c30354 --- /dev/null +++ b/app/views/print_templates_pdfme/viewer.html.erb @@ -0,0 +1,21 @@ + + + + <% plugin_name = Redmine::Plugin.find(:redmine_print_templates).name rescue 'Print Templates' %> + <%= "#{plugin_name} - Viewer" %> + + <%= stylesheet_link_tag('print_pdfme', plugin: 'redmine_print_templates') %> + + +
+
<%= l(:reload_page_for_designer) %>
+
+ + <%= javascript_include_tag 'pdfme-viewer', plugin: :redmine_print_templates %> + + diff --git a/assets/javascripts/print_templates_designer.js b/assets/javascripts/print_templates_designer.js index 6499034..067a54f 100644 --- a/assets/javascripts/print_templates_designer.js +++ b/assets/javascripts/print_templates_designer.js @@ -1,206 +1,163 @@ -document.addEventListener("DOMContentLoaded", function() { - const basepdfField = document.getElementById('print_template_basepdf'); - const schemasField = document.getElementById('print_template_schemas'); - const inputsField = document.getElementById('print_template_inputs'); - const trackerIdSelect = document.getElementById('print_template_tracker_id'); - const openBtn = document.getElementById('open-designer-fullscreen-btn'); - const closeBtn = document.getElementById('close-designer-fullscreen-btn'); - const designerOverlay = document.getElementById('designer-fullscreen'); - const iframe = document.getElementById('pdfme-designer-iframe'); - const uploadField = document.getElementById('pdf-upload'); - const useBlankPdfLink = document.getElementById('use-blank-pdf'); - const fieldsDropdown = document.getElementById('tracker-fields'); - const addFieldBtn = document.getElementById('add-field-btn'); - const templateDownloadBtn = document.getElementById('template_download-designer-fullscreen-btn'); - const templateUploadBtn = document.getElementById('template_upload-designer-fullscreen-btn'); - const templateFileInput = document.getElementById('template-file-input'); - - // Function to update fields dropdown - function loadTrackerData() { - if (trackerIdSelect) { +document.addEventListener("DOMContentLoaded", () => { + const elements = { + basepdfField: document.getElementById('print_template_basepdf'), + schemasField: document.getElementById('print_template_schemas'), + trackerIdSelect: document.getElementById('print_template_tracker_id'), + openBtn: document.getElementById('open-designer-fullscreen-btn'), + closeBtn: document.getElementById('close-designer-fullscreen-btn'), + designerOverlay: document.getElementById('designer-fullscreen'), + iframe: document.getElementById('pdfme-designer-iframe'), + uploadField: document.getElementById('pdf-upload'), + useBlankPdfLink: document.getElementById('use-blank-pdf'), + templateDownloadBtn: document.getElementById('template_download-designer-fullscreen-btn'), + templateUploadBtn: document.getElementById('template_upload-designer-fullscreen-btn'), + templateFileInput: document.getElementById('template-file-input'), + basepdfIcon: document.getElementById('basepdf-ok-icon') + }; + + // console.log('Print Templates Designer loaded!'); + + const showError = (message) => { + console.error(message); + alert('An error occurred. Please try again.'); + }; + + const loadTrackerData = () => { + if (elements.trackerIdSelect) { Rails.ajax({ - url: trackerIdSelect.getAttribute('data-url') + "?tracker_id=" + trackerIdSelect.value, + url: `${elements.trackerIdSelect.getAttribute('data-url')}?tracker_id=${elements.trackerIdSelect.value}`, type: 'GET', - dataType: 'script' + dataType: 'json', + success: (response) => { + sessionStorage.setItem('fieldKeyOptions', JSON.stringify(response.fieldKeyOptions)); + sessionStorage.setItem('fieldFormatOptions', JSON.stringify(response.fieldFormatOptions)); + } }); } - } - - // Event listener for tracker dropdown change - if (trackerIdSelect) { - trackerIdSelect.addEventListener('change', function() { - loadTrackerData(); - }); - - // Trigger the loadTrackerData request on page load - loadTrackerData(); - } - - // Function to get the selected field's details - function getSelectedFieldDetails() { - const selectedOption = fieldsDropdown.options[fieldsDropdown.selectedIndex]; - return { - identifier: selectedOption.value, - name: selectedOption.text, - format: selectedOption.getAttribute('data-format') - }; - } - - if (templateDownloadBtn) { - templateDownloadBtn.addEventListener('click', function() { - const iframeWindow = document.getElementById('pdfme-designer-iframe').contentWindow; - const trackerSelect = document.getElementById('print_template_tracker_id'); - const trackerName = trackerSelect.options[trackerSelect.selectedIndex].text; - - iframeWindow.postMessage({ - type: 'triggerDownloadTemplate', - data: { trackerName: trackerName } // Enclose trackerName within a data object - }, window.location.origin); - }); - } - - if (templateUploadBtn && templateFileInput) { - templateUploadBtn.addEventListener('click', function() { - // Trigger the hidden file input when the button is clicked - document.getElementById('template-file-input').click(); - }); - - // Assuming BLANK_PDF_DATA_URL is the known blank PDF data URL from PDFme - const BLANK_PDF_DATA_URL = "data:application/pdf;base64,JVBERi0xLjcKJeLjz9MKNSAwIG9iago8PAovRmlsdGVyIC9GbGF0ZURlY29kZQovTGVuZ3RoIDM4Cj4+CnN0cmVhbQp4nCvkMlAwUDC1NNUzMVGwMDHUszRSKErlCtfiyuMK5AIAXQ8GCgplbmRzdHJlYW0KZW5kb2JqCjQgMCBvYmoKPDwKL1R5cGUgL1BhZ2UKL01lZGlhQm94IFswIDAgNTk1LjQ0IDg0MS45Ml0KL1Jlc291cmNlcyA8PAo+PgovQ29udGVudHMgNSAwIFIKL1BhcmVudCAyIDAgUgo+PgplbmRvYmoKMiAwIG9iago8PAovVHlwZSAvUGFnZXMKL0tpZHMgWzQgMCBSXQovQ291bnQgMQo+PgplbmRvYmoKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovUGFnZXMgMiAwIFIKPj4KZW5kb2JqCjMgMCBvYmoKPDwKL3RyYXBwZWQgKGZhbHNlKQovQ3JlYXRvciAoU2VyaWYgQWZmaW5pdHkgRGVzaWduZXIgMS4xMC40KQovVGl0bGUgKFVudGl0bGVkLnBkZikKL0NyZWF0aW9uRGF0ZSAoRDoyMDIyMDEwNjE0MDg1OCswOScwMCcpCi9Qcm9kdWNlciAoaUxvdmVQREYpCi9Nb2REYXRlIChEOjIwMjIwMTA2MDUwOTA5WikKPj4KZW5kb2JqCjYgMCBvYmoKPDwKL1NpemUgNwovUm9vdCAxIDAgUgovSW5mbyAzIDAgUgovSUQgWzwyODhCM0VENTAyOEU0MDcyNERBNzNCOUE0Nzk4OUEwQT4gPEY1RkJGNjg4NkVERDZBQUNBNDRCNEZDRjBBRDUxRDlDPl0KL1R5cGUgL1hSZWYKL1cgWzEgMiAyXQovRmlsdGVyIC9GbGF0ZURlY29kZQovSW5kZXggWzAgN10KL0xlbmd0aCAzNgo+PgpzdHJlYW0KeJxjYGD4/5+RUZmBgZHhFZBgDAGxakAEP5BgEmFgAABlRwQJCmVuZHN0cmVhbQplbmRvYmoKc3RhcnR4cmVmCjUzMgolJUVPRgo="; - - templateFileInput.addEventListener('change', function(event) { - const file = event.target.files[0]; - if (file && file.type === "application/json") { - const reader = new FileReader(); - reader.onload = function(e) { - try { - const templateData = JSON.parse(e.target.result); - - // Check if uploaded 'basePdf' is the same as the blank PDF data URL - if (templateData.basePdf === BLANK_PDF_DATA_URL) { - basepdfField.value = ''; - } else if (templateData.basePdf) { - basepdfField.value = templateData.basePdf; - } else { - basepdfField.value = ''; // Reset if 'basePdf' is not provided - } - - // Toggle visibility of basepdf controls - toggleBasePDFControls(); - - // Post the remaining data to the iframe - const iframeWindow = document.getElementById('pdfme-designer-iframe').contentWindow; - iframeWindow.postMessage({ - type: 'loadTemplate', - data: { templateData: templateData } - }, window.location.origin); - } catch (error) { - console.error('Failed to parse template file:', error); - alert('Invalid JSON template file.'); + }; + + const handleTemplateDownloadClick = () => { + const iframeWindow = elements.iframe.contentWindow; + const trackerName = elements.trackerIdSelect.options[elements.trackerIdSelect.selectedIndex].text; + + iframeWindow.postMessage({ + type: 'downloadTemplate', + data: { trackerName: trackerName } + }, window.location.origin); + }; + + const handleTemplateUploadClick = () => { + elements.templateFileInput.click(); + }; + + const handleTemplateFileChange = (event) => { + const file = event.target.files[0]; + if (file && file.type === "application/json") { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const templateData = JSON.parse(e.target.result); + + if (templateData.basePdf) { + elements.basepdfField.value = templateData.basePdf; + } else { + elements.basepdfField.value = ''; // Reset if 'basePdf' is not provided } - }; - reader.readAsText(file); - } else { - alert('Please upload a valid JSON template file.'); - } - }); - } - - // Function to encode a PDF file in base64 - function encodeBasePDF(input) { + + toggleBasePDFControls(); + + const iframeWindow = elements.iframe.contentWindow; + iframeWindow.postMessage({ + type: 'uploadTemplate', + data: { templateData: templateData } + }, window.location.origin); + } catch (error) { + showError('Failed to parse template file: ' + error); + } + }; + reader.readAsText(file); + } else { + alert('Please upload a valid JSON template file.'); + } + }; + + const encodeBasePDF = (input) => { if (input.files && input.files[0]) { const reader = new FileReader(); - reader.onload = function(e) { + reader.onload = (e) => { const encodedPDF = e.target.result; - if (basepdfField) { - basepdfField.value = encodedPDF; + if (elements.basepdfField) { + elements.basepdfField.value = encodedPDF; } toggleBasePDFControls(); }; reader.readAsDataURL(input.files[0]); } - } - - // Function to toggle the visibility of the basepdf controls - function toggleBasePDFControls() { - const basepdfIcon = document.getElementById('basepdf-ok-icon'); + }; - if (basepdfField && basepdfField.value) { - basepdfIcon.style.display = 'inline'; - useBlankPdfLink.style.display = 'inline'; + const toggleBasePDFControls = () => { + if (elements.basepdfField && elements.basepdfField.value) { + elements.basepdfIcon.style.display = 'inline'; + elements.useBlankPdfLink.style.display = 'inline'; } else { - basepdfIcon.style.display = 'none'; - useBlankPdfLink.style.display = 'none'; + elements.basepdfIcon.style.display = 'none'; + elements.useBlankPdfLink.style.display = 'none'; } - } - - // Event listener for file upload - if (uploadField) { - uploadField.addEventListener('change', function() { - encodeBasePDF(this); - }); - } - - // Event listener for resetting the PDF - if (useBlankPdfLink) { - useBlankPdfLink.addEventListener('click', function(event) { - event.preventDefault(); - if (basepdfField) { - basepdfField.value = ''; - uploadField.value = ''; - toggleBasePDFControls(); - } - }); - } - - // Event listeners for iframe communication - if (basepdfField && schemasField && inputsField && openBtn && closeBtn && designerOverlay && iframe) { - openBtn.addEventListener('click', function() { - designerOverlay.style.display = 'block'; - const iframeWindow = iframe.contentWindow; - - // Parse schemas and inputs into arrays - const parsedSchemas = schemasField.value ? JSON.parse(schemasField.value) : []; - const parsedInputs = inputsField.value ? JSON.parse(inputsField.value) : [{}]; - - iframeWindow.postMessage({ - type: 'initialData', - data: { - basePdf: basepdfField.value, - schemas: parsedSchemas, - inputs: parsedInputs - } - }, window.location.origin); - }); - - closeBtn.addEventListener('click', function() { - designerOverlay.style.display = 'none'; - iframe.src = iframe.src; // Refresh the iframe - }); - - window.addEventListener('message', function(event) { - if (event.origin !== window.location.origin) { - return; - } - - if (event.data.type === 'updateData') { - // Only updating schemas and inputs - const { schemas, inputs } = event.data.data; - schemasField.value = JSON.stringify(schemas); - inputsField.value = JSON.stringify(inputs); - } - }); - } - - if (addFieldBtn && fieldsDropdown && iframe) { - addFieldBtn.addEventListener('click', function() { - const selectedField = getSelectedFieldDetails(); - const iframeWindow = iframe.contentWindow; - - iframeWindow.postMessage({ - type: 'addField', - data: selectedField - }, window.location.origin); - }); - } + }; + + const handleOpenBtnClick = () => { + elements.designerOverlay.style.display = 'block'; + const iframeWindow = elements.iframe.contentWindow; + + const data = {}; + elements.basepdfField.value ? data.basePdf = elements.basepdfField.value : null; + elements.schemasField.value ? data.schemas = JSON.parse(elements.schemasField.value) : null; + + data.fieldKeyOptions = JSON.parse(sessionStorage.getItem('fieldKeyOptions')); + data.fieldFormatOptions = JSON.parse(sessionStorage.getItem('fieldFormatOptions')); + + iframeWindow.postMessage({ + type: 'openDesigner', + data: data + }, window.location.origin); + }; + + const handleCloseBtnClick = () => { + elements.designerOverlay.style.display = 'none'; + elements.iframe.src = elements.iframe.src; // Refresh the iframe + }; + + const handleMessageEvent = (event) => { + if (event.origin !== window.location.origin) return; + + if (event.data.type === 'updateData') { + const { schemas } = event.data.data; + elements.schemasField.value = JSON.stringify(schemas); + } + }; + + elements.trackerIdSelect?.addEventListener('change', loadTrackerData); + loadTrackerData(); + + elements.templateDownloadBtn?.addEventListener('click', handleTemplateDownloadClick); + elements.templateUploadBtn?.addEventListener('click', handleTemplateUploadClick); + elements.templateFileInput?.addEventListener('change', handleTemplateFileChange); + + elements.uploadField?.addEventListener('change', function() { + encodeBasePDF(this); + }); + + elements.useBlankPdfLink?.addEventListener('click', (event) => { + event.preventDefault(); + if (elements.basepdfField) { + elements.basepdfField.value = ''; + elements.uploadField.value = ''; + toggleBasePDFControls(); + } + }); + + elements.openBtn?.addEventListener('click', handleOpenBtnClick); + elements.closeBtn?.addEventListener('click', handleCloseBtnClick); + + window.addEventListener('message', handleMessageEvent); }); diff --git a/assets/javascripts/print_templates_font.js b/assets/javascripts/print_templates_font.js index 17d4c8e..3a354b0 100644 --- a/assets/javascripts/print_templates_font.js +++ b/assets/javascripts/print_templates_font.js @@ -66,6 +66,8 @@ function clearFontFields() { document.addEventListener('DOMContentLoaded', function() { const container = document.querySelector('#settings.plugin.plugin-redmine_print_templates'); + // console.log('Print Templates Fonts loaded!'); + if (container) { const form = container.querySelector('form'); diff --git a/assets/javascripts/print_templates_form.js b/assets/javascripts/print_templates_form.js deleted file mode 100644 index e9704fb..0000000 --- a/assets/javascripts/print_templates_form.js +++ /dev/null @@ -1,53 +0,0 @@ -document.addEventListener("DOMContentLoaded", function() { - const viewPdfButton = document.getElementById('open-form-fullscreen-btn'); - const printTemplateSelect = document.getElementById('print_template_select'); - - const formOverlay = document.getElementById('form-fullscreen'); - const downloadPdfButton = document.getElementById('download-pdf-button'); - const closeFormBtn = document.getElementById('close-form-fullscreen-btn'); - const formIframe = document.getElementById('pdfme-form-iframe'); - - if (viewPdfButton && printTemplateSelect) { - viewPdfButton.addEventListener('click', function() { - const selectedTemplateId = printTemplateSelect.value; - if (selectedTemplateId) { - // Fetch the template data from the server - Rails.ajax({ - url: `/print_templates/show/${selectedTemplateId}.json`, - type: 'GET', - dataType: 'json', - success: function(response) { - // Open the form overlay and send the fetched template data - formOverlay.style.display = 'block'; - formIframe.contentWindow.postMessage({ - type: 'loadSelectedTemplate', - templateData: response - }, window.location.origin); - }, - error: function(error) { - console.error('Error fetching template data:', error); - } - }); - } else { - alert('Please select a print template.'); - } - }); - } - - if (downloadPdfButton) { - downloadPdfButton.addEventListener('click', function() { - if (formIframe && formIframe.contentWindow) { - // Send a message to the iframe to trigger PDF generation - formIframe.contentWindow.postMessage({ type: 'generatePdf' }, window.location.origin); - } - }); - } - - // Close button logic - if (closeFormBtn) { - closeFormBtn.addEventListener('click', function() { - formOverlay.style.display = 'none'; - formIframe.src = formIframe.src; // Refresh the iframe - }); - } -}); diff --git a/assets/javascripts/print_templates_viewer.js b/assets/javascripts/print_templates_viewer.js new file mode 100644 index 0000000..e6dab25 --- /dev/null +++ b/assets/javascripts/print_templates_viewer.js @@ -0,0 +1,67 @@ +document.addEventListener("DOMContentLoaded", () => { + const elements = { + viewPdfButton: document.getElementById('open-form-fullscreen-btn'), + printTemplateSelect: document.getElementById('print_template_select'), + formOverlay: document.getElementById('form-fullscreen'), + downloadPdfButton: document.getElementById('download-pdf-button'), + closeFormBtn: document.getElementById('close-form-fullscreen-btn'), + formIframe: document.getElementById('pdfme-form-iframe') + }; + + // console.log('Print Templates Viewer loaded!'); + + const showError = (message) => { + console.error(message); + alert('An error occurred. Please try again.'); + }; + + const handleViewPdfClick = () => { + const selectedTemplateId = elements.printTemplateSelect.value; + if (!selectedTemplateId) { + alert('Please select a print template.'); + return; + } + + Rails.ajax({ + url: `/print_templates/show/${selectedTemplateId}.json`, + type: 'GET', + dataType: 'json', + success: (response) => { + const data = {}; + data.basePdf = response.basepdf; + data.schemas = response.schemas; + data.fieldKeyOptions = response.fieldKeyOptions; + data.fieldFormatOptions = response.fieldFormatOptions; + + elements.formOverlay.style.display = 'block'; + elements.formIframe.contentWindow.postMessage({ + type: 'openViewer', + data: data + }, window.location.origin); + }, + error: (error) => { + showError('Error fetching template data: ' + error); + } + }); + }; + + const handleDownloadPdfClick = () => { + if (elements.formIframe?.contentWindow) { + elements.formIframe.contentWindow.postMessage({ + type: 'generatePdf', + data: { + download: true + } + }, window.location.origin); + } + }; + + const handleCloseFormClick = () => { + elements.formOverlay.style.display = 'none'; + elements.formIframe.src = elements.formIframe.src; // Refresh the iframe + }; + + elements.viewPdfButton?.addEventListener('click', handleViewPdfClick); + elements.downloadPdfButton?.addEventListener('click', handleDownloadPdfClick); + elements.closeFormBtn?.addEventListener('click', handleCloseFormClick); +}); diff --git a/config/locales/de.yml b/config/locales/de.yml new file mode 100644 index 0000000..346523b --- /dev/null +++ b/config/locales/de.yml @@ -0,0 +1 @@ +de: diff --git a/config/locales/en.yml b/config/locales/en.yml index bca58b6..8821ff3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -9,6 +9,10 @@ en: label_print_templates_new: "New print template" label_print_template_name: "Print template name" label_print_template_basepdf: "BasePDF" + label_print_template_context: "Context" + label_print_template_context_option_issue: "Issue (single)" + label_print_template_context_option_issues: "Issues (list)" + label_print_template_context_option_project: "Project" general_print_templates_settings: "General Settings" button_print_template_designer_open: "Open Designer in Fullscreen" button_print_template_add_field: "Add Field" @@ -31,11 +35,17 @@ en: print_templates_settings_general_placeholder_empty: "Empty field placeholder text" print_templates_settings_general_placeholder_note: "Enter text to display in empty fields, or leave this blank to keep fields empty." + label_core_fields: "Core Fields" + label_custom_fields: "Custom Fields" + label_special_fields: "Special Fields" + pdf_properties_print_templates_settings: "PDF Properties" print_templates_settings_general_pdf_author: "Author" print_templates_settings_general_pdf_creator: "Creator" print_templates_settings_general_pdf_producer: "Producer" + reload_page_for_designer: "Reload page to start PDFme Designer..." + # javascript: confirm_delete_font: "Are you sure you want to delete this font?" error_uploading_font: "Error during font upload." diff --git a/config/routes.rb b/config/routes.rb index 818d291..70e801f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,7 +4,7 @@ resources :print_templates, only: %i(index new create edit update destroy) do # Nested routes for PDFme functionalities get 'designer', to: 'print_templates_pdfme#designer', on: :collection - get 'form', to: 'print_templates_pdfme#form', on: :collection + get 'viewer', to: 'print_templates_pdfme#viewer', on: :collection end # Route to fetch fields for a tracker (AJAX) diff --git a/db/migrate/20240626160439_remove_inputs_from_print_templates.rb b/db/migrate/20240626160439_remove_inputs_from_print_templates.rb new file mode 100644 index 0000000..b178657 --- /dev/null +++ b/db/migrate/20240626160439_remove_inputs_from_print_templates.rb @@ -0,0 +1,5 @@ +class RemoveInputsFromPrintTemplates < ActiveRecord::Migration[6.1] + def change + remove_column :print_templates, :inputs, :jsonb + end +end diff --git a/db/migrate/20240628132633_add_context_to_print_templates.rb b/db/migrate/20240628132633_add_context_to_print_templates.rb new file mode 100644 index 0000000..6b802b9 --- /dev/null +++ b/db/migrate/20240628132633_add_context_to_print_templates.rb @@ -0,0 +1,5 @@ +class AddContextToPrintTemplates < ActiveRecord::Migration[6.1] + def change + add_column :print_templates, :context, :string, default: 'issue', null: false + end +end diff --git a/docs/ReferenceTemplate.json b/docs/ReferenceTemplate.json index 5ea7de4..caf3432 100644 --- a/docs/ReferenceTemplate.json +++ b/docs/ReferenceTemplate.json @@ -1,9 +1,14 @@ { - "basePdf": "data:application/pdf;base64,JVBERi0xLjcKJeLjz9MKNSAwIG9iago8PAovRmlsdGVyIC9GbGF0ZURlY29kZQovTGVuZ3RoIDM4Cj4+CnN0cmVhbQp4nCvkMlAwUDC1NNUzMVGwMDHUszRSKErlCtfiyuMK5AIAXQ8GCgplbmRzdHJlYW0KZW5kb2JqCjQgMCBvYmoKPDwKL1R5cGUgL1BhZ2UKL01lZGlhQm94IFswIDAgNTk1LjQ0IDg0MS45Ml0KL1Jlc291cmNlcyA8PAo+PgovQ29udGVudHMgNSAwIFIKL1BhcmVudCAyIDAgUgo+PgplbmRvYmoKMiAwIG9iago8PAovVHlwZSAvUGFnZXMKL0tpZHMgWzQgMCBSXQovQ291bnQgMQo+PgplbmRvYmoKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovUGFnZXMgMiAwIFIKPj4KZW5kb2JqCjMgMCBvYmoKPDwKL3RyYXBwZWQgKGZhbHNlKQovQ3JlYXRvciAoU2VyaWYgQWZmaW5pdHkgRGVzaWduZXIgMS4xMC40KQovVGl0bGUgKFVudGl0bGVkLnBkZikKL0NyZWF0aW9uRGF0ZSAoRDoyMDIyMDEwNjE0MDg1OCswOScwMCcpCi9Qcm9kdWNlciAoaUxvdmVQREYpCi9Nb2REYXRlIChEOjIwMjIwMTA2MDUwOTA5WikKPj4KZW5kb2JqCjYgMCBvYmoKPDwKL1NpemUgNwovUm9vdCAxIDAgUgovSW5mbyAzIDAgUgovSUQgWzwyODhCM0VENTAyOEU0MDcyNERBNzNCOUE0Nzk4OUEwQT4gPEY1RkJGNjg4NkVERDZBQUNBNDRCNEZDRjBBRDUxRDlDPl0KL1R5cGUgL1hSZWYKL1cgWzEgMiAyXQovRmlsdGVyIC9GbGF0ZURlY29kZQovSW5kZXggWzAgN10KL0xlbmd0aCAzNgo+PgpzdHJlYW0KeJxjYGD4/5+RUZmBgZHhFZBgDAGxakAEP5BgEmFgAABlRwQJCmVuZHN0cmVhbQplbmRvYmoKc3RhcnR4cmVmCjUzMgolJUVPRgo=", + "basePdf": { + "width": 210, + "height": 297, + "padding": [0, 0, 0, 0] + }, "schemas": [ { - "$(standard#author.name)#1e69": { - "type": "text", + "author.name": { + "type": "extendedText", + "content": "Author", "position": { "x": 73.25, "y": 45.77 @@ -14,8 +19,9 @@ "verticalAlignment": "middle", "fontName": "Roboto" }, - "$(standard#status.name)#a1a2": { - "type": "text", + "status.name": { + "type": "extendedText", + "content": "Status", "position": { "x": 73.25, "y": 58.11 @@ -26,8 +32,9 @@ "verticalAlignment": "middle", "fontName": "Roboto" }, - "$(standard#priority.name)#075f": { - "type": "text", + "priority.name": { + "type": "extendedText", + "content": "Priority", "position": { "x": 73.25, "y": 70.45 @@ -38,8 +45,9 @@ "verticalAlignment": "middle", "fontName": "Roboto" }, - "$(standard#assigned_to.name)#5b13": { - "type": "text", + "assigned_to.name": { + "type": "extendedText", + "content": "Assigned to", "position": { "x": 73.25, "y": 82.78 @@ -50,8 +58,9 @@ "verticalAlignment": "middle", "fontName": "Roboto" }, - "$(standard#category.name)#4bf9": { - "type": "text", + "category.name": { + "type": "extendedText", + "content": "Category", "position": { "x": 73.25, "y": 95.12 @@ -62,8 +71,9 @@ "verticalAlignment": "middle", "fontName": "Roboto" }, - "$(standard#fixed_version.name)#0a2d": { - "type": "text", + "fixed_version.name": { + "type": "extendedText", + "content": "Fixed version", "position": { "x": 73.25, "y": 107.46 @@ -74,8 +84,9 @@ "verticalAlignment": "middle", "fontName": "Roboto" }, - "$(standard#subject)#e114": { - "type": "text", + "subject": { + "type": "extendedText", + "content": "Subject", "position": { "x": 73.25, "y": 119.79 @@ -86,8 +97,9 @@ "verticalAlignment": "middle", "fontName": "Roboto" }, - "$(standard#start_date)#800c": { - "type": "text", + "start_date": { + "type": "extendedText", + "content": "Start date", "position": { "x": 73.25, "y": 132.13 @@ -98,8 +110,9 @@ "verticalAlignment": "middle", "fontName": "Roboto" }, - "$(standard#due_date)#ecc1": { - "type": "text", + "due_date": { + "type": "extendedText", + "content": "Due date", "position": { "x": 73.25, "y": 144.47 @@ -110,8 +123,9 @@ "verticalAlignment": "middle", "fontName": "Roboto" }, - "$(standard#done_ratio)#ab49": { - "type": "text", + "done_ratio": { + "type": "extendedText", + "content": "Done ratio", "position": { "x": 73.25, "y": 156.81 @@ -122,8 +136,9 @@ "verticalAlignment": "middle", "fontName": "Roboto" }, - "$(standard#estimated_hours)#e114": { - "type": "text", + "estimated_hours": { + "type": "extendedText", + "content": "Estimated hours", "position": { "x": 73.25, "y": 169.15 @@ -134,8 +149,9 @@ "verticalAlignment": "middle", "fontName": "Roboto" }, - "$(standard#total_estimated_hours)#a814": { - "type": "text", + "total_estimated_hours": { + "type": "extendedText", + "content": "Total estimated hours", "position": { "x": 73.25, "y": 181.48 @@ -146,8 +162,9 @@ "verticalAlignment": "middle", "fontName": "Roboto" }, - "$(standard#spent_hours)#4995": { - "type": "text", + "spent_hours": { + "type": "extendedText", + "content": "Spent hours", "position": { "x": 73.25, "y": 193.82 @@ -158,8 +175,9 @@ "verticalAlignment": "middle", "fontName": "Roboto" }, - "$(standard#total_spent_hours)#32c2": { - "type": "text", + "total_spent_hours": { + "type": "extendedText", + "content": "Total spent hours", "position": { "x": 73.25, "y": 206.16 @@ -170,8 +188,9 @@ "verticalAlignment": "middle", "fontName": "Roboto" }, - "$(standard#created_on)#83d2": { - "type": "text", + "created_on": { + "type": "extendedText", + "content": "Created on", "position": { "x": 73.25, "y": 218.5 @@ -182,8 +201,9 @@ "verticalAlignment": "middle", "fontName": "Roboto" }, - "$(standard#updated_on)#ca96": { - "type": "text", + "updated_on": { + "type": "extendedText", + "content": "Updated on", "position": { "x": 73.25, "y": 230.83 @@ -194,8 +214,9 @@ "verticalAlignment": "middle", "fontName": "Roboto" }, - "$(standard#closed_on)#0b1a": { - "type": "text", + "closed_on": { + "type": "extendedText", + "content": "Closed on", "position": { "x": 73.25, "y": 243.17 @@ -208,8 +229,9 @@ } }, { - "$(standard#description)#62b3": { - "type": "text", + "description": { + "type": "extendedText", + "content": "Description", "position": { "x": 74.51, "y": 30.06 @@ -223,53 +245,6 @@ }, {}, {}, - { - "$(special#issue_url)#dc83": { - "type": "qrcode", - "position": { - "x": 153.75, - "y": 35.86 - }, - "width": 30, - "height": 30, - "alignment": "left", - "verticalAlignment": "middle" - }, - "$(special#issue_map)#1722": { - "type": "image", - "position": { - "x": 25.75, - "y": 70.99 - }, - "width": 158, - "height": 100, - "alignment": "left", - "verticalAlignment": "middle" - } - } - ], - "sampledata": [ - { - "$(standard#assigned_to.name)#5b13": "$(standard#assigned_to.name)", - "$(standard#author.name)#1e69": "$(standard#author.name)", - "$(standard#category.name)#4bf9": "$(standard#category.name)", - "$(standard#closed_on)#0b1a": "$(standard#closed_on)", - "$(standard#created_on)#83d2": "$(standard#created_on)", - "$(standard#description)#62b3": "$(standard#description)", - "$(standard#done_ratio)#ab49": "$(standard#done_ratio)", - "$(standard#due_date)#ecc1": "$(standard#due_date)", - "$(standard#estimated_hours)#e114": "$(standard#estimated_hours)", - "$(standard#fixed_version.name)#0a2d": "$(standard#fixed_version.name)", - "$(standard#priority.name)#075f": "$(standard#priority.name)", - "$(standard#spent_hours)#4995": "$(standard#spent_hours)", - "$(standard#start_date)#800c": "$(standard#start_date)", - "$(standard#status.name)#a1a2": "$(standard#status.name)", - "$(standard#subject)#e114": "$(standard#subject)", - "$(standard#total_estimated_hours)#a814": "$(standard#total_estimated_hours)", - "$(standard#total_spent_hours)#32c2": "$(standard#total_spent_hours)", - "$(standard#updated_on)#ca96": "$(standard#updated_on)", - "$(special#issue_map)#1722": "", - "$(special#issue_url)#dc83": "localhost:3000" - } + {} ] } diff --git a/init.rb b/init.rb index 28db560..a780ea9 100644 --- a/init.rb +++ b/init.rb @@ -7,7 +7,7 @@ author_url 'https://github.com/georepublic' url 'https://github.com/gtt-project/redmine_print_templates' description 'Enables printing templates with PDFme in Redmine deployments' - version '0.2.0' + version '0.3.0' requires_redmine :version_or_higher => '5.0.0' diff --git a/lib/redmine_print_templates/view_hooks.rb b/lib/redmine_print_templates/view_hooks.rb index 1432d28..0ed75f5 100644 --- a/lib/redmine_print_templates/view_hooks.rb +++ b/lib/redmine_print_templates/view_hooks.rb @@ -3,13 +3,21 @@ class ViewHooks < Redmine::Hook::ViewListener def view_layouts_base_body_bottom(context={}) tags = [] - if User.current.admin? - tags << javascript_include_tag('print_templates_font.js', plugin: 'redmine_print_templates') + controller = context[:controller] + + # Load specific JS for admin in print_templates menu + if User.current.admin? && controller.controller_name == 'print_templates' && ['index', 'new', 'edit'].include?(controller.action_name) tags << javascript_include_tag('print_templates_designer.js', plugin: 'redmine_print_templates') + + # Load print_templates_font.js in plugin settings + elsif User.current.admin? && controller.controller_name == 'settings' && controller.action_name == 'plugin' && controller.params[:id] == 'redmine_print_templates' + tags << javascript_include_tag('print_templates_font.js', plugin: 'redmine_print_templates') + + # Load viewer JS only if designer JS is not loaded + elsif User.current.allowed_to?(:view_print_templates, context[:project], global: true) + tags << javascript_include_tag('print_templates_viewer.js', plugin: 'redmine_print_templates') end - if User.current.allowed_to?(:view_print_templates, context[:project], global: true) - tags << javascript_include_tag('print_templates_form.js', plugin: 'redmine_print_templates') - end + tags.join("\n") end diff --git a/package.json b/package.json index 4c09022..10a5cdd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redmine_print_templates", - "version": "0.2.0", + "version": "0.3.0", "description": "Plugin that enables printing templates with pdfme in Redmine", "main": "index.js", "type": "module", @@ -23,15 +23,17 @@ }, "homepage": "https://github.com/gtt-project/redmine_print_templates#readme", "dependencies": { - "@pdfme/common": "3.1.2", - "@pdfme/generator": "3.1.2", - "@pdfme/schemas": "3.1.2", - "@pdfme/ui": "3.1.2", - "@types/uuid": "^9.0.7", + "@pdfme/common": "4.0.2", + "@pdfme/generator": "4.0.2", + "@pdfme/schemas": "4.0.2", + "@pdfme/ui": "4.0.2", + "@types/fontkit": "^2.0.7", + "@types/uuid": "^10.0.0", + "signature_pad": "^5.0.2", "ts-loader": "^9.5.1", - "typescript": "^5.3.2", - "uuid": "^9.0.1", - "webpack": "^5.89.0", + "typescript": "^5.5.2", + "uuid": "^10.0.0", + "webpack": "^5.92.1", "webpack-cli": "^5.1.4" } } diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..04d9694 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,19 @@ +import { Template, PDFME_VERSION } from '@pdfme/common'; + +export const supportedLocales = ['en', 'ja', 'ar', 'th', 'it', 'pl', 'zh', 'ko', 'de', 'es', 'fr'] as const; + +export const defaultTemplate: Template = { + basePdf: { + width: 210, + height: 297, + padding: [0, 0, 0, 0] + }, + schemas: [{} as Record], + pdfmeVersion: PDFME_VERSION +}; + +export const themeSettings = { + token: { + colorPrimary: '#f1515c' + }, +}; diff --git a/src/designer.ts b/src/designer.ts index 0ae6cee..896ac77 100644 --- a/src/designer.ts +++ b/src/designer.ts @@ -1,70 +1,15 @@ -import { Template, BLANK_PDF, getDefaultFont } from '@pdfme/common'; import { Designer } from '@pdfme/ui'; -import { text, image, barcodes } from "@pdfme/schemas"; -import { initializeOrUpdateDesigner, addFieldToDesigner, downloadTemplate, loadTemplate } from './designerUtils'; -import { validateLocale, SupportedLocale } from './locales'; - -interface FontData { - name: string; - url: string; -} - -declare const embeddedFonts: FontData[]; +import { openDesigner, downloadTemplate, uploadTemplate } from '.'; document.addEventListener("DOMContentLoaded", function() { - const container = document.getElementById('pdfme-container'); - let designer: Designer | undefined; - - if (!designer && container) { - const htmlLang = document.documentElement.lang || 'en'; - const locale = htmlLang.split('-')[0]; // Extract the language part - const validatedLocale: SupportedLocale = validateLocale(locale); - - async function initializeDesigner() { - // Set the default fonts - const availableFonts = getDefaultFont(); - - for (const font of embeddedFonts) { - const response = await fetch(font.url); - const arrayBuffer = await response.arrayBuffer(); - - availableFonts[font.name] = { - data: arrayBuffer, - // include fallback and subset options if necessary - }; - } - designer = new Designer({ - domContainer: container, - template: { basePdf: BLANK_PDF, schemas: [], sampledata: [{}] }, - plugins: { text, image, qrcode: barcodes.qrcode }, - options: { - lang: validatedLocale, - theme: { - token: { - colorPrimary: '#f1515c' - }, - }, - font: availableFonts - }, - }); - - designer.onChangeTemplate((updatedTemplate: Template) => { - window.parent.postMessage({ - type: 'updateData', - data: { - schemas: updatedTemplate.schemas, - inputs: updatedTemplate.sampledata || [{}] - } - }, window.location.origin); - }); - } + let designer: Designer | undefined; - initializeDesigner(); - } + const htmllang = document.documentElement.lang || 'en'; + const locale = htmllang.split('-')[0]; // Extract the language part // Listen for messages from the parent page - window.addEventListener('message', function(event) { + window.addEventListener('message', async function(event) { if (event.origin !== window.location.origin) { return; } @@ -72,18 +17,27 @@ document.addEventListener("DOMContentLoaded", function() { const { type, data } = event.data; switch (type) { - case 'initialData': - const { basePdf, schemas, inputs } = data; - designer = initializeOrUpdateDesigner(designer, container, basePdf, schemas, inputs); - break; - case 'addField': - addFieldToDesigner(designer, data); + case 'openDesigner': + designer = await openDesigner({ + container: document.getElementById('pdfme-container'), + template: { + basePdf: data.basePdf, + schemas: data.schemas, + }, + locale: locale, + fieldKeyOptions: data.fieldKeyOptions, + fieldFormatOptions: data.fieldFormatOptions, + }); break; - case 'triggerDownloadTemplate': - downloadTemplate(designer, data.trackerName); + case 'downloadTemplate': + if (designer) { + downloadTemplate(designer, data.trackerName); + } break; - case 'loadTemplate': - loadTemplate(designer, data.templateData); + case 'uploadTemplate': + if (designer) { + uploadTemplate(designer, data.templateData); + } break; // Add other cases as needed } diff --git a/src/designerUtils.ts b/src/designerUtils.ts deleted file mode 100644 index f2219e7..0000000 --- a/src/designerUtils.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { Template, BLANK_PDF } from '@pdfme/common'; -import { Designer } from '@pdfme/ui'; -import { text, image, barcodes } from "@pdfme/schemas"; -import { v4 as uuidv4 } from 'uuid'; - -import { mapIconDataUrl } from './mapIconDataUrl'; -type FieldType = 'text' | 'image' | 'qrcode'; - -export function initializeOrUpdateDesigner(designer: Designer | undefined, container: HTMLElement | null, basePdf: string, schemas: any[], inputs: any[]): Designer | undefined { - const template: Template = { - basePdf: basePdf || BLANK_PDF, - schemas: schemas || [], - sampledata: inputs || [{}], - }; - - if (!designer && container) { - designer = new Designer({ - domContainer: container, - template: template, - plugins: { text, image, qrcode: barcodes.qrcode }, - }); - - designer.onChangeTemplate((updatedTemplate) => { - window.parent.postMessage({ - type: 'updateData', - data: { - schemas: updatedTemplate.schemas, - inputs: updatedTemplate.sampledata || [{}] - } - }, window.location.origin); - }); - } else { - designer?.updateTemplate(template); - } - - // Return the Designer instance - return designer; -} - -export function addFieldToDesigner(designer: Designer, fieldData: any) { - // Fetch the current template state - const currentTemplate = designer.getTemplate(); - - // Create a deep clone of the current schemas and sampledata to avoid direct mutation - const updatedSchemas = JSON.parse(JSON.stringify(currentTemplate.schemas)); - const updatedSampledata = JSON.parse(JSON.stringify(currentTemplate.sampledata)); - - const fieldType: FieldType = (fieldData.format && ['text', 'image', 'qrcode'].includes(fieldData.format)) ? fieldData.format as FieldType : 'text'; - const defaultFieldData = getDefaultFieldData(fieldType); - - const newSchema = { - type: fieldType, - position: { - x: (fieldData.x !== undefined) ? fieldData.x : defaultFieldData.x, - y: (fieldData.y !== undefined) ? fieldData.y : defaultFieldData.y - }, - width: (fieldData.width !== undefined) ? fieldData.width : defaultFieldData.width, - height: (fieldData.height !== undefined) ? fieldData.height : defaultFieldData.height, - alignment: fieldData.alignment || 'left', - verticalAlignment: fieldData.verticalAlignment || 'middle', - }; - - // Generate a unique key for the new schema - const newSchemaKey = `$(${fieldData.identifier})#${uuidv4().substring(0, 4)}`; - - // Generate a value for the sampledata without the UUID - let sampleDataValue: string; - if (fieldType === 'image') { - sampleDataValue = mapIconDataUrl; - } else if (fieldType === 'qrcode') { - sampleDataValue = window.location.host; - } else { - // For other types, use the standard sample data value - sampleDataValue = `$(${fieldData.identifier})`; - } - - // Add the new schema to the updatedSchemas - if (!updatedSchemas[0]) { - updatedSchemas[0] = {}; - } - updatedSchemas[0][newSchemaKey] = newSchema; - - // Add an entry for the new field in the sampledata - if (updatedSampledata.length === 0) { - updatedSampledata.push({}); - } - updatedSampledata[0][newSchemaKey] = sampleDataValue; // Set default value for the new field - - // Update the Designer instance with the new template - designer.updateTemplate({ - ...currentTemplate, - schemas: updatedSchemas, - sampledata: updatedSampledata - }); -} - -function getDefaultFieldData(type: FieldType) { - const defaults: Record = { - text: { width: 80, height: 10, x: 0, y: 0 }, - image: { width: 40, height: 40, x: 0, y: 0 }, - qrcode: { width: 30, height: 30, x: 0, y: 0 } - }; - - return defaults[type] || defaults['text']; -} - -export function downloadTemplate(designer: Designer, trackerName: string) { - if (designer) { - const templateData = designer.getTemplate(); - if (templateData) { - // Format the filename with the tracker name - const fileName = `template_${trackerName.toLowerCase().replace(/\s+/g, '_')}`; - downloadJsonFile(templateData, fileName); - } - } -} - -export function downloadJsonFile(data: any, fileName: string) { - const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `${fileName}.json`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); -} - -export function loadTemplate(designer: Designer, templateData: Template) { - if (designer) { - designer.updateTemplate(templateData); - } -} diff --git a/src/form.ts b/src/form.ts deleted file mode 100644 index edddce8..0000000 --- a/src/form.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { Template, GeneratorOptions, BLANK_PDF, getDefaultFont } from '@pdfme/common'; -import { Form } from '@pdfme/ui'; -import { text, image, barcodes } from "@pdfme/schemas"; -import { generate } from '@pdfme/generator'; - -interface FontData { - name: string; - url: string; -} - -declare const embeddedFonts: FontData[]; -declare const issueData: any; -declare const pluginSettings: any; - -const urlParams = new URLSearchParams(window.location.search); -const issueId = urlParams.get('issue_id'); - -document.addEventListener("DOMContentLoaded", function() { - const container = document.getElementById('pdfme-container'); - let form: Form | undefined; - - window.addEventListener('message', async function(event) { - if (event.origin !== window.location.origin) { - return; - } - - const { type, templateData } = event.data; - - switch (type) { - case 'loadSelectedTemplate': - if (container && templateData) { - // Parse schemas and inputs from JSON strings to JavaScript arrays - const schemas = JSON.parse(templateData.schemas || '[]'); - const inputs = [mapIssueDataToTemplate(issueData, schemas)] || [{}]; - - // Use BLANK_PDF as a fallback if basePdf is not provided - const basePdf = templateData.basepdf || BLANK_PDF; - - // Define the template structure - const template: Template = { - basePdf: basePdf, - schemas: schemas - }; - - async function initializeForm() { - // Set the default fonts - const availableFonts = getDefaultFont(); - - for (const font of embeddedFonts) { - const response = await fetch(font.url); - const arrayBuffer = await response.arrayBuffer(); - - availableFonts[font.name] = { - data: arrayBuffer, - }; - } - - // Recreate the Form instance with the new template and inputs - form = new Form({ - domContainer: container, - template: template, - inputs: inputs, - plugins: { text, image, qrcode: barcodes.qrcode }, - options: { - theme: { - token: { - colorPrimary: '#f1515c' - }, - }, - font: availableFonts, - }, - }); - } - - initializeForm(); - } - break; - - case 'generatePdf': - if (form) { - const currentInputs = form.getInputs(); - - async function initializeGenerator() { - // Set the default fonts - const availableFonts = getDefaultFont(); - - for (const font of embeddedFonts) { - const response = await fetch(font.url); - const arrayBuffer = await response.arrayBuffer(); - - availableFonts[font.name] = { - data: arrayBuffer, - // include fallback and subset options if necessary - }; - } - - generate({ - template: form.getTemplate(), - inputs: currentInputs, - plugins: { text, image, qrcode: barcodes.qrcode }, - options: { - font: availableFonts, - author: pluginSettings.default_pdf_author || "", - creator: pluginSettings.default_pdf_creator || "", - keywords: [], - language: "en-US", - producer: pluginSettings.default_pdf_producer || "", - subject: "Redmine Issue Report", - title: `Issue #${issueId}: Feature Implementation`, - }, - }).then((pdf) => { - const blob = new Blob([pdf.buffer], { type: 'application/pdf' }); - const timestamp = new Date().toISOString().replace(/[-:.TZ]/g, ''); - - // Create and trigger download link - const downloadLink = document.createElement("a"); - downloadLink.href = URL.createObjectURL(blob); - downloadLink.download = `${timestamp}_issue_${issueId}.pdf`; - document.body.appendChild(downloadLink); - downloadLink.click(); - document.body.removeChild(downloadLink); - URL.revokeObjectURL(downloadLink.href); - }).catch((error) => { - console.error('Error generating PDF:', error); - }); - } - - initializeGenerator(); - } - break; - // Handle other cases if necessary - } - }); -}); - -function mapIssueDataToTemplate(issueData: any, template: any) { - const mappedInputs: any = {}; - - template.forEach((page: any) => { - Object.keys(page).forEach(key => { - const match = key.match(/\$\(([^#]+)#([^)]+)\)/); - if (match && match.length >= 3) { - const type = match[1]; - const field = match[2]; - let value = ''; - - switch (type) { - case 'standard': - const fields = field.split('.'); - value = fields.reduce((acc, curr) => acc && acc[curr], issueData.issue); - break; - case 'custom': - const customField = issueData.issue.custom_fields.find((f: any) => f.name === field); - value = customField ? customField.value : ''; - break; - case 'special': - // Handle special cases here - break; - default: - // Handle unknown type - break; - } - - if (value !== undefined) { - mappedInputs[key] = String(value); - } - } - }); - }); - - return mappedInputs; -} diff --git a/src/helper.ts b/src/helper.ts new file mode 100644 index 0000000..4466547 --- /dev/null +++ b/src/helper.ts @@ -0,0 +1,161 @@ +import { Template, getDefaultFont } from '@pdfme/common'; +import { defaultTemplate, supportedLocales } from './constants'; +import type { SupportedLocale } from './types'; + +declare const embeddedFonts: any[]; +declare const pluginSettings: any; + +/** + * Validates the given locale and returns a supported locale. + * If the given locale is not supported, 'en' (English) is returned as the default locale. + * + * @param locale - The locale to validate. + * @returns The validated supported locale. + */ +export function validateLocale(locale: string): SupportedLocale { + return supportedLocales.includes(locale as SupportedLocale) ? locale as SupportedLocale : 'en'; +} + +/** + * Checks if a value is valid. + * @param value - The value to check. + * @returns Whether the value is valid or not. + */ +function isValidValue(value: T | null | undefined | ''): value is T { + return value !== undefined && value !== null && value !== ''; +} + +/** + * Get available fonts + * @returns A promise that resolves to the available fonts. + */ +export async function getAvailableFonts(): Promise> { + const availableFonts = getDefaultFont(); + + for (const font of embeddedFonts) { + const response = await fetch(font.url); + const arrayBuffer = await response.arrayBuffer(); + + availableFonts[font.name] = { + data: arrayBuffer, + // include fallback and subset options if necessary + }; + } + + return availableFonts; +} + +/** + * Creates a template object based on the provided partial template. + * @param template - The partial template object. + * @returns The final template object. + */ +export function createTemplate(template: Partial