diff --git a/app/models/archive_file.rb b/app/models/archive_file.rb index 601654f..afbd5f0 100644 --- a/app/models/archive_file.rb +++ b/app/models/archive_file.rb @@ -85,6 +85,10 @@ class ArchiveFile field :uid, type: String field :sort_number, type: Integer field :tmp_sort_number, type: Integer + field :enable_file_link_sort, type: Boolean, default: false + field :sort_order_list, type: Hash, default: {} + field :files_links_migrated, type: Boolean, default: false + attr_accessor :order_source field :rss2_sn field :feed_uid field :site_feed_id @@ -96,15 +100,15 @@ class ArchiveFile # belongs_to :archive_file_category has_many :archive_file_multiples, :autosave => true, :dependent => :destroy - + has_many :archive_file_links, :autosave => true, :dependent => :destroy accepts_nested_attributes_for :archive_file_multiples, :allow_destroy => true - + accepts_nested_attributes_for :archive_file_links, :allow_destroy => true # validates :title, :at_least_one => true - after_save :save_archive_file_multiples, :update_tmp_sort_number - + after_save :save_archive_file_multiples, :save_archive_file_links, :sync_links_back_to_urls_array, :sync_file_order_between_ui, :update_tmp_sort_number + before_save :add_http - + before_save :migrate_urls_to_links_if_needed after_initialize do unless self.new_record? if self.urls.nil? && self.url.present? @@ -139,6 +143,67 @@ class ArchiveFile self.class.recalc_sort_number end end + + def migrate_urls_to_links_if_needed + return unless self.enable_file_link_sort + return if self.files_links_migrated + in_use_locales = (Site.first.in_use_locales rescue I18n.available_locales).map(&:to_s) + in_use_locales.each do |locale| + urls = self.urls_translations[locale] || [] + url_texts = self.url_texts_translations[locale] || [] + urls.each_with_index do |url, i| + next if url.blank? + self.archive_file_links.build(locale: locale, url: url, url_text: url_texts[i]) + end + end + new_sort_order_list = {} + in_use_locales.each do |locale| + file_tokens = self.archive_file_multiples.select { |f| f.choose_lang.include?(locale) } + .sort_by { |f| -(f.sort_number.to_i) } + .map { |f| "file:#{f.id}" } + link_tokens = self.archive_file_links.select { |l| l.locale == locale } + .map { |l| "link:#{l.id}" } + new_sort_order_list[locale] = file_tokens + link_tokens + end + self.sort_order_list = new_sort_order_list + self.files_links_migrated = true + end + + def ordered_tokens_for_locale(locale) + locale = locale.to_s + order_tokens = (self.sort_order_list[locale] || []).dup + existing_file_tokens = self.archive_file_multiples.select { |f| f.choose_lang.include?(locale) }.map { |f| "file:#{f.id}" } + existing_link_tokens = self.archive_file_links.select { |l| l.locale == locale }.map { |l| "link:#{l.id}" } + valid_tokens = existing_file_tokens + existing_link_tokens + tokens = order_tokens & valid_tokens + missing = valid_tokens - tokens + tokens + missing + end + + def merged_items_for_locale(locale) + locale = locale.to_s + file_map = self.archive_file_multiples.each_with_object({}) { |f, h| h[f.id.to_s] = f } + link_map = self.archive_file_links.select { |l| l.locale == locale }.each_with_object({}) { |l, h| h[l.id.to_s] = l } + self.ordered_tokens_for_locale(locale).map do |token| + type, id = token.split(":", 2) + if type == "file" + f = file_map[id] + next nil if f.nil? + { type: "file", token: token, label: (f.file_title.presence || (File.basename(f.file.path) rescue "file")) } + else + l = link_map[id] + next nil if l.nil? + { type: "link", token: token, label: (l.url_text.presence || l.url) } + end + end.compact + end + + def save_archive_file_links + return if @skip_callback + self.archive_file_links.each do |t| + t.destroy if t.should_destroy + end + end def get_url_text(idx=0,org=false) url_text = self.url_texts[idx] rescue nil if org @@ -272,6 +337,7 @@ class ArchiveFile end ArchiveCache.destroy_all end + def get_files(locale=nil, serial_number=0) if locale.nil? locale = I18n.locale.to_s @@ -286,6 +352,45 @@ class ArchiveFile h["serial_number"] = serial_number h end + elsif self.enable_file_link_sort + file_map = self.archive_file_multiples.each_with_object({}) { |f, h| h[f.id.to_s] = f } + link_map = self.archive_file_links.each_with_object({}) { |l, h| h[l.id.to_s] = l } + self.ordered_tokens_for_locale(locale).each do |token| + type, id = token.split(":", 2) + if type == "file" + file = file_map[id] + next if file.nil? + title = (file.file_title.blank? ? File.basename(file.file.path) : file.file_title) rescue "" + extension = file.file.file.extension.downcase rescue "" + serial_number += 1 + real_file_time = "" + if file.file.present? && file.file.path && File.exist?(file.file.path) + real_file_time = File.mtime(file.file.path).localtime.strftime("%Y-%m-%d %H:%M") + else + real_file_time = file.created_at.localtime.strftime("%Y-%m-%d %H:%M") rescue "" + end + files << { + "file-name" => title, + "file-type" => extension, + "file-url" => (file.file.present? ? "/xhr/archive/download?file=#{file.id}" : 'javascript:void(0)'), + "target" => "_blank", + "serial_number" => serial_number, + "file-date" => real_file_time + } + elsif type == "link" + link = link_map[id] + next if link.nil? + serial_number += 1 + target = (link.url.to_s.match(/\/[^\/]/) ? '_self' : '_blank') + files << { + "file-name" => (link.url_text.presence || link.url), + "file-type" => 'link', + "file-url" => link.url.to_s + "\" data-target=\"#{target}", + "target" => target, + "serial_number" => serial_number + } + end + end else self.archive_file_multiples.order_by(:sort_number=>'desc').each do |file| if file.choose_lang.include?(locale) @@ -296,7 +401,6 @@ class ArchiveFile if file.file.present? && file.file.path && File.exist?(file.file.path) real_file_time = File.mtime(file.file.path).localtime.strftime("%Y-%m-%d %H:%M") else - # 如果找不到實體檔案,才回退到資料庫時間(或留空) real_file_time = file.created_at.localtime.strftime("%Y-%m-%d %H:%M") rescue "" end files << { @@ -325,6 +429,7 @@ class ArchiveFile end [files, serial_number] end + def get_widget_data(locale=nil, serial_number=0, idx=0, show_tags=false, more_url=nil) created_at_int = self.created_at.strftime('%Y%m%d').to_i statuses = self.statuses_with_classname.collect do |status| @@ -546,6 +651,70 @@ class ArchiveFile end end end + + def sync_links_back_to_urls_array + return unless self.enable_file_link_sort + in_use_locales = (Site.first.in_use_locales rescue I18n.available_locales).map(&:to_s) + new_urls = self.urls_translations.dup + new_url_texts = self.url_texts_translations.dup + in_use_locales.each do |locale| + ordered_links = self.ordered_tokens_for_locale(locale) + .select { |token| token.start_with?("link:") } + .map { |token| token.split(":", 2)[1] } + link_map = self.archive_file_links.select { |l| l.locale == locale }.each_with_object({}) { |l, h| h[l.id.to_s] = l } + urls_for_locale = [] + url_texts_for_locale = [] + ordered_links.each do |id| + link = link_map[id] + next if link.nil? + urls_for_locale << link.url + url_texts_for_locale << link.url_text + end + new_urls[locale] = urls_for_locale + new_url_texts[locale] = url_texts_for_locale + end + @skip_callback = true + in_use_locales.each do |locale| + I18n.with_locale(locale) do + self.class.where(:id => self.id).update_all(:urls => new_urls[locale], :url_texts => new_url_texts[locale]) + end + end + @skip_callback = false + end + + def sync_file_order_between_ui + return unless self.enable_file_link_sort + case self.order_source + when "merged_list" + reference_locale = (Site.first.in_use_locales rescue I18n.available_locales).first.to_s + file_ids_in_order = self.ordered_tokens_for_locale(reference_locale) + .select { |token| token.start_with?("file:") } + .map { |token| token.split(":", 2)[1] } + total = file_ids_in_order.count + file_ids_in_order.each_with_index do |id, idx| + ArchiveFileMultiple.where(:id => id).update_all(:sort_number => (total - idx)) + end + self.class.recalc_sort_number + when "file_sort" + in_use_locales = (Site.first.in_use_locales rescue I18n.available_locales).map(&:to_s) + new_sort_order_list = self.sort_order_list.dup + in_use_locales.each do |locale| + tokens = self.ordered_tokens_for_locale(locale) + file_slot_indexes = tokens.each_index.select { |i| tokens[i].start_with?("file:") } + visible_files_sorted = self.archive_file_multiples.select { |f| f.choose_lang.include?(locale) } + .sort_by { |f| -(f.sort_number.to_i) } + new_tokens = tokens.dup + file_slot_indexes.each_with_index do |slot_index, i| + file = visible_files_sorted[i] + new_tokens[slot_index] = "file:#{file.id}" if file + end + new_sort_order_list[locale] = new_tokens + end + self.class.where(:id => self.id).update_all(:sort_order_list => new_sort_order_list) + else + end + end + def self.smart_convertor(text,url) doc = Nokogiri.HTML(text) doc.search('a[href]').each do |link| diff --git a/app/models/archive_file_link.rb b/app/models/archive_file_link.rb new file mode 100644 index 0000000..79b0920 --- /dev/null +++ b/app/models/archive_file_link.rb @@ -0,0 +1,14 @@ +class ArchiveFileLink + include Mongoid::Document + include Mongoid::Timestamps + + field :locale, type: String + field :url, type: String + field :url_text, type: String + field :sort_number, type: Integer + field :should_destroy, type: Boolean + + belongs_to :archive_file, index: true + + index({archive_file_id: 1, locale: 1}) +end diff --git a/app/views/admin/archive_files/_form.html.erb b/app/views/admin/archive_files/_form.html.erb index f1fa7bd..627a53f 100644 --- a/app/views/admin/archive_files/_form.html.erb +++ b/app/views/admin/archive_files/_form.html.erb @@ -103,7 +103,6 @@

<%= t('archive.min') %>: <%=min_sort_number%>, <%= t('archive.max') %>: <%=max_sort_number%>

- @@ -150,6 +149,20 @@ <% end %> + +
@@ -175,25 +188,52 @@ <% end %>
+
- +
+ +
<% end %> -
@@ -232,6 +272,7 @@
+ <%= hidden_field_tag 'archive_file[order_source]', '', id: 'archive_file_order_source' %> <%= hidden_field_tag 'page', params[:page] if !params[:page].blank? %> <%= f.submit t('submit'), class: 'btn btn-primary' %> <%= t("archive.back_to_archives") %> @@ -424,6 +465,7 @@ existingfiles.each(function(i, file){ $(file).find("input.file-sort-number-field").val(existingfiles.length - i); }) + $('#archive_file_order_source').val('file_sort'); } }); $('.main-forms .add-on').tooltip(); @@ -452,6 +494,63 @@ } }); + function toggle_file_link_sort_ui(){ + var migrated = $('#file_link_sort_control').data('migrated') === true || $('#file_link_sort_control').data('migrated') === 'true'; + if (!migrated){ + $('.old-style-links').show(); + $('.new-style-links').hide(); + $('.file-link-order-block').hide(); + return; + } + var checked = $('#enable_file_link_sort_checkbox').is(':checked'); + $('.old-style-links').toggle(!checked); + $('.new-style-links').toggle(checked); + $('.file-link-order-block').toggle(checked); + } + toggle_file_link_sort_ui(); + $(document).on('change', '#enable_file_link_sort_checkbox', toggle_file_link_sort_ui); + + // 合併排序清單:拖曳後,把目前順序寫回 hidden fields 一起送出 + $(".file-link-sortable").each(function(){ + var $list = $(this); + var locale = $list.data("locale"); + function write_hidden_fields(){ + var $hidden_container = $list.siblings(".file-link-sortable-hidden-fields"); + $hidden_container.empty(); + $list.children("li").each(function(){ + var token = $(this).data("token"); + $hidden_container.append( + $("", {type:"hidden", name:"archive_file[sort_order_list]["+locale+"][]", value: token}) + ); + }); + } + $list.sortable({ update: function(){ + write_hidden_fields(); + $('#archive_file_order_source').val('merged_list'); + }}); + write_hidden_fields(); + }); + + // 新式連結:新增一筆 + $(document).on('click', '.add_file_link', function(){ + var target_locale = $(this).data('locale'); + var new_index = 'new_' + new Date().getTime(); + var html = "<%= escape_javascript(f.fields_for(:archive_file_links, ArchiveFileLink.new, child_index: 'NEW_INDEX') { |lf| render(partial: 'form_file_link', locals: {f: lf, locale: 'new_locale', form_link: ArchiveFileLink.new}) }) %>"; + html = html.replace(/NEW_INDEX/g, new_index).replace(/new_locale/g, target_locale); + $(this).siblings('.new-style-links').append(html); + }); + // 新式連結:刪除「這次新增、還沒存檔」的那筆 + $(document).on('click', '.delete_file_link', function(){ + $(this).parents('.input-prepend').remove(); + }); + // 新式連結:刪除「已存在」的那筆(標記 should_destroy,交給後端在儲存時真正刪除) + $(document).on('click', '.remove_existing_file_link', function(){ + if(confirm("<%= I18n.t(:sure?)%>")){ + $(this).find('.should_destroy').val('1'); + $(this).hide(); + } + }); + }); <% end %> diff --git a/app/views/admin/archive_files/_form_file_link.html.erb b/app/views/admin/archive_files/_form_file_link.html.erb new file mode 100644 index 0000000..e99117f --- /dev/null +++ b/app/views/admin/archive_files/_form_file_link.html.erb @@ -0,0 +1,18 @@ +
+ + <%= f.text_field :url, class: "input-large", placeholder: t(:url) %> + "> + <%= f.text_field :url_text, class: "input-large", placeholder: t("archive.url_text_hint") %> + <%= f.hidden_field :locale, value: locale %> + <% if form_link.new_record? %> + + + + <% else %> + <%= f.hidden_field :id %> + <%= f.hidden_field :should_destroy, value: nil, class: 'should_destroy' %> + + + + <% end %> +
diff --git a/config/locales/en.yml b/config/locales/en.yml index 8773562..66c3dbc 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -39,8 +39,14 @@ en: Title: Title Files: Files Category: Category + enable_file_link_sort: "File & Link Sorting" + enable_file_link_sort_hint: "When enabled, files and links can be freely mixed and reordered together using the list below" + file_link_order: "Order (drag items to reorder)" + file_link_order_hint: "Only saved files/links can be dragged here; please save once first for newly added items, then reopen this page to reorder them." + not_migrated_yet_hint: "This record hasn't been migrated for sorting yet. The link display will stay as-is for now; please check the box and save once, then reopen this page to see and use the ordering list." + disable_file_link_sort_warning: "Note: After disabling this option, the displayed order of files and links may differ from your sorted result. Please save and re-open this page to review the order again." back_to_archives: Back to Archives frontend: archive: Archive Front-end widget: - index: Archive Widget \ No newline at end of file + index: Archive Widget diff --git a/config/locales/zh_tw.yml b/config/locales/zh_tw.yml index b974dca..fe376b9 100644 --- a/config/locales/zh_tw.yml +++ b/config/locales/zh_tw.yml @@ -36,6 +36,12 @@ zh_tw: downloaded_times: 下載次數 Files: 檔案 Category: 類別 + enable_file_link_sort: "檔案及連結排序" + enable_file_link_sort_hint: "開啟後,「檔案」與「連結」可以合併、自由交錯排序,顯示順序將依照下方拖曳的順序呈現" + file_link_order: "排序(拖曳項目調整順序)" + file_link_order_hint: "只有已經儲存過的檔案/連結才能在這裡拖曳排序;新增的項目請先按「儲存」,再重新進入編輯頁面排序。" + not_migrated_yet_hint: "此筆資料尚未套用過排序功能,畫面暫時維持原本的連結顯示方式;請先勾選並儲存一次,儲存後重新進入編輯頁面,才會看到排序清單並生效。" + disable_file_link_sort_warning: "提醒:關閉此開關後,檔案及連結的顯示順序可能會與您排序的結果有差異,請送出(儲存)後再次進入編輯頁面確認排序。" back_to_archives: 返回檔案 frontend: archive: 檔案室前台