diff --git a/app/controllers/api/form_events_controller.rb b/app/controllers/api/form_events_controller.rb index 3e8e2a38..759cd230 100644 --- a/app/controllers/api/form_events_controller.rb +++ b/app/controllers/api/form_events_controller.rb @@ -11,8 +11,9 @@ module Api params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present? submitters = paginate( - submitters.preload(template: :folder, submission: [:submitters, { audit_trail_attachment: :blob, - combined_document_attachment: :blob }], + submitters.preload(template: { folder: :parent_folder }, + submission: [:submitters, { audit_trail_attachment: :blob, + combined_document_attachment: :blob }], documents_attachments: :blob, attachments_attachments: :blob), field: :completed_at ) diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index 0cf7d2ba..77751066 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -14,7 +14,7 @@ module Api submissions = filter_submissions(submissions, params) submissions = paginate(submissions.preload(:created_by_user, :submitters, - template: :folder, + template: { folder: :parent_folder }, combined_document_attachment: :blob, audit_trail_attachment: :blob)) @@ -104,9 +104,10 @@ module Api submissions = submissions.where(slug: params[:slug]) if params[:slug].present? if params[:template_folder].present? - folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:template_folder]).pluck(:id) + folders = + TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:template_folder]) - submissions = submissions.joins(:template).where(template: { folder_id: folder_ids }) + submissions = submissions.joins(:template).where(template: { folder_id: folders.pluck(:id) }) end if params.key?(:archived) diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index a45f5c6c..0ccc5b39 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -7,7 +7,7 @@ module Api def index templates = filter_templates(@templates, params) - templates = paginate(templates.preload(:author, :folder)) + templates = paginate(templates.preload(:author, folder: :parent_folder)) schema_documents = ActiveStorage::Attachment.where(record_id: templates.map(&:id), @@ -92,9 +92,9 @@ module Api templates = templates.where(slug: params[:slug]) if params[:slug].present? if params[:folder].present? - folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:folder]).pluck(:id) + folders = TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:folder]) - templates = templates.where(folder_id: folder_ids) + templates = templates.where(folder_id: folders.pluck(:id)) end templates diff --git a/app/controllers/template_folders_autocomplete_controller.rb b/app/controllers/template_folders_autocomplete_controller.rb index 120fe8dc..2bfdec99 100644 --- a/app/controllers/template_folders_autocomplete_controller.rb +++ b/app/controllers/template_folders_autocomplete_controller.rb @@ -3,14 +3,31 @@ class TemplateFoldersAutocompleteController < ApplicationController load_and_authorize_resource :template_folder, parent: false - LIMIT = 100 + LIMIT = 30 def index - templates_query = Template.accessible_by(current_ability).where(archived_at: nil) + parent_name, name = + if params[:parent_name].present? + [params[:parent_name], params[:q]] + else + params[:q].to_s.split(' /', 2).map(&:squish) + end - template_folders = @template_folders.where(id: templates_query.select(:folder_id)) - template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT) + if name + parent_folder = @template_folders.find_by(name: parent_name, parent_folder_id: nil) + else + name = parent_name + end - render json: template_folders.as_json(only: %i[name archived_at]) + template_folders = TemplateFolders.filter_active_folders(@template_folders.where(parent_folder:), + Template.accessible_by(current_ability)) + + name = name.to_s.downcase + + template_folders = TemplateFolders.search(template_folders, name).order(id: :desc).limit(LIMIT) + + render json: template_folders.preload(:parent_folder) + .sort_by { |e| e.name.downcase.index(name) || Float::MAX } + .as_json(only: %i[name archived_at], methods: %i[full_name]) end end diff --git a/app/controllers/template_folders_controller.rb b/app/controllers/template_folders_controller.rb index 850a9046..25e489ce 100644 --- a/app/controllers/template_folders_controller.rb +++ b/app/controllers/template_folders_controller.rb @@ -5,13 +5,39 @@ class TemplateFoldersController < ApplicationController helper_method :selected_order + TEMPLATES_PER_PAGE = 12 + FOLDERS_PER_PAGE = 18 + def show - @templates = @template_folder.templates.active.accessible_by(current_ability) - .preload(:author, :template_accesses) - @templates = Templates.search(current_user, @templates, params[:q]) - @templates = Templates::Order.call(@templates, current_user, selected_order) + @templates = Template.active.accessible_by(current_ability) + .where(folder: [@template_folder, *(params[:q].present? ? @template_folder.subfolders : [])]) + .preload(:author, :template_accesses) + + @template_folders = + @template_folder.subfolders.where(id: Template.accessible_by(current_ability).active.select(:folder_id)) + + @template_folders = TemplateFolders.search(@template_folders, params[:q]) + @template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order) + + if @templates.exists? + @templates = Templates.search(current_user, @templates, params[:q]) + @templates = Templates::Order.call(@templates, current_user, selected_order) - @pagy, @templates = pagy_auto(@templates, limit: 12) + limit = + if @template_folders.size < 4 + TEMPLATES_PER_PAGE + else + (@template_folders.size < 7 ? 9 : 6) + end + + @pagy, @templates = pagy_auto(@templates, limit:) + + load_related_submissions if params[:q].present? && @templates.blank? + else + @pagy, @template_folders = pagy(@template_folders, limit: FOLDERS_PER_PAGE) + + @templates = @templates.none + end end def edit; end @@ -40,4 +66,21 @@ class TemplateFoldersController < ApplicationController def template_folder_params params.require(:template_folder).permit(:name) end + + def load_related_submissions + @related_submissions = + Submission.accessible_by(current_ability) + .where(archived_at: nil) + .where(template_id: current_account.templates.active + .where(folder: [@template_folder, *@template_folder.subfolders]) + .select(:id)) + .preload(:template_accesses, :created_by_user, + template: :author, + submitters: :start_form_submission_events) + + @related_submissions = Submissions.search(current_user, @related_submissions, params[:q]) + .order(id: :desc) + + @related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5) + end end diff --git a/app/controllers/templates_archived_controller.rb b/app/controllers/templates_archived_controller.rb index 34d2b601..69b16936 100644 --- a/app/controllers/templates_archived_controller.rb +++ b/app/controllers/templates_archived_controller.rb @@ -4,9 +4,27 @@ class TemplatesArchivedController < ApplicationController load_and_authorize_resource :template, parent: false def index - @templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc) + @templates = @templates.where.not(archived_at: nil) + .preload(:author, :template_accesses, folder: :parent_folder) + .order(id: :desc) + @templates = Templates.search(current_user, @templates, params[:q]) @pagy, @templates = pagy_auto(@templates, limit: 12) + + return unless params[:q].present? && @templates.blank? + + @related_submissions = + Submission.accessible_by(current_ability) + .joins(:template) + .where.not(templates: { archived_at: nil }) + .preload(:template_accesses, :created_by_user, + template: :author, + submitters: :start_form_submission_events) + + @related_submissions = Submissions.search(current_user, @related_submissions, params[:q]) + .order(id: :desc) + + @related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5) end end diff --git a/app/controllers/templates_dashboard_controller.rb b/app/controllers/templates_dashboard_controller.rb index 218c89e7..4497038e 100644 --- a/app/controllers/templates_dashboard_controller.rb +++ b/app/controllers/templates_dashboard_controller.rb @@ -11,10 +11,11 @@ class TemplatesDashboardController < ApplicationController helper_method :selected_order def index - @template_folders = @template_folders.where(id: @templates.active.select(:folder_id)) + @template_folders = + TemplateFolders.filter_active_folders(@template_folders.where(parent_folder_id: nil), @templates) @template_folders = TemplateFolders.search(@template_folders, params[:q]) - @template_folders = sort_template_folders(@template_folders, current_user, selected_order) + @template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order) @pagy, @template_folders = pagy( @template_folders, @@ -68,40 +69,6 @@ class TemplatesDashboardController < ApplicationController Templates.search(current_user, rel, params[:q]) end - def sort_template_folders(template_folders, current_user, order) - case order - when 'used_at' - subquery = - Template.left_joins(:submissions) - .group(:folder_id) - .where(account_id: current_user.account_id) - .select( - :folder_id, - Template.arel_table[:updated_at].maximum.as('updated_at_max'), - Submission.arel_table[:created_at].maximum.as('submission_created_at_max') - ) - - template_folders = template_folders.joins( - Template.arel_table - .join(subquery.arel.as('templates'), Arel::Nodes::OuterJoin) - .on(TemplateFolder.arel_table[:id].eq(Template.arel_table[:folder_id])) - .join_sources - ) - - template_folders.order( - Arel::Nodes::Case.new - .when(Template.arel_table[:submission_created_at_max].gt(Template.arel_table[:updated_at_max])) - .then(Template.arel_table[:submission_created_at_max]) - .else(Template.arel_table[:updated_at_max]) - .desc - ) - when 'name' - template_folders.order(name: :asc) - else - template_folders.order(id: :desc) - end - end - def selected_order @selected_order ||= if cookies.permanent[:dashboard_templates_order].blank? || diff --git a/app/controllers/templates_folders_controller.rb b/app/controllers/templates_folders_controller.rb index 3fdcc873..3e83023a 100644 --- a/app/controllers/templates_folders_controller.rb +++ b/app/controllers/templates_folders_controller.rb @@ -6,7 +6,9 @@ class TemplatesFoldersController < ApplicationController def edit; end def update - @template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:name]) + name = [params[:parent_name], params[:name]].compact_blank.join(' / ') + + @template.folder = TemplateFolders.find_or_create_by_name(current_user, name) if @template.save redirect_back(fallback_location: template_path(@template), notice: I18n.t('document_template_has_been_moved')) diff --git a/app/javascript/elements/dashboard_dropzone.js b/app/javascript/elements/dashboard_dropzone.js index 888c99ee..886aae37 100644 --- a/app/javascript/elements/dashboard_dropzone.js +++ b/app/javascript/elements/dashboard_dropzone.js @@ -15,6 +15,7 @@ export default targetable(class extends HTMLElement { static [target.static] = [ 'form', 'fileDropzone', + 'folderDropzone', 'fileDropzoneLoading' ] @@ -25,12 +26,13 @@ export default targetable(class extends HTMLElement { window.addEventListener('dragleave', this.onWindowDragleave) this.fileDropzone?.addEventListener('drop', this.onDropFile) + this.folderDropzone?.addEventListener('drop', this.onDropNewFolder) this.folderCards.forEach((el) => el.addEventListener('drop', (e) => this.onDropFolder(e, el))) this.templateCards.forEach((el) => el.addEventListener('drop', this.onDropTemplate)) this.templateCards.forEach((el) => el.addEventListener('dragstart', this.onTemplateDragStart)) - return [this.fileDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => { + return [this.fileDropzone, this.folderDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => { el?.addEventListener('dragover', this.onDragover) el?.addEventListener('dragleave', this.onDragleave) }) @@ -46,6 +48,10 @@ export default targetable(class extends HTMLElement { onTemplateDragStart = (e) => { const id = e.target.href.split('/').pop() + this.folderCards.forEach((el) => el.classList.remove('bg-base-200', 'before:hidden')) + this.folderDropzone?.classList?.remove('hidden') + window.flash?.remove() + e.dataTransfer.effectAllowed = 'move' if (id) { @@ -104,7 +110,7 @@ export default targetable(class extends HTMLElement { } else { const formData = new FormData() - formData.append('name', el.innerText.trim()) + formData.append('name', el.dataset.fullName) fetch(`/templates/${templateId}/folder`, { method: 'PUT', @@ -176,6 +182,24 @@ export default targetable(class extends HTMLElement { } } + onDropNewFolder (e) { + e.preventDefault() + + const templateId = e.dataTransfer.getData('template_id') + + const a = document.createElement('a') + + a.href = `/templates/${templateId}/folder/edit?autocomplete=false` + a.dataset.turboFrame = 'modal' + a.classList.add('hidden') + + document.body.append(a) + + a.click() + + a.remove() + } + onDragleave () { this.style.backgroundColor = null @@ -199,6 +223,7 @@ export default targetable(class extends HTMLElement { this.isDrag = true + window.flash?.remove() this.fileDropzone?.classList?.remove('hidden') this.hiddenOnDrag.forEach((el) => { el.style.display = 'none' }) @@ -212,6 +237,7 @@ export default targetable(class extends HTMLElement { this.isDrag = false this.fileDropzone?.classList?.add('hidden') + this.folderDropzone?.classList?.add('hidden') this.hiddenOnDrag.forEach((el) => { el.style.display = null }) diff --git a/app/javascript/elements/folder_autocomplete.js b/app/javascript/elements/folder_autocomplete.js index 2ef977c5..de2a5069 100644 --- a/app/javascript/elements/folder_autocomplete.js +++ b/app/javascript/elements/folder_autocomplete.js @@ -2,6 +2,8 @@ import autocomplete from 'autocompleter' export default class extends HTMLElement { connectedCallback () { + if (this.dataset.enabled === 'false') return + autocomplete({ input: this.input, preventSubmit: this.dataset.submitOnSelect === 'true' ? 0 : 1, @@ -14,12 +16,16 @@ export default class extends HTMLElement { } onSelect = (item) => { - this.input.value = item.name + this.input.value = this.dataset.parentName ? item.name : item.full_name } fetch = (text, resolve) => { const queryParams = new URLSearchParams({ q: text }) + if (this.dataset.parentName) { + queryParams.append('parent_name', this.dataset.parentName) + } + fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => { const items = await resp.json() @@ -34,7 +40,7 @@ export default class extends HTMLElement { div.setAttribute('dir', 'auto') - div.textContent = item.name + div.textContent = this.dataset.parentName ? item.name : item.full_name return div } diff --git a/app/javascript/elements/page_container.js b/app/javascript/elements/page_container.js index d909ae7f..ca5221ba 100644 --- a/app/javascript/elements/page_container.js +++ b/app/javascript/elements/page_container.js @@ -1,14 +1,12 @@ export default class extends HTMLElement { connectedCallback () { - this.image.addEventListener('load', (e) => { - this.image.setAttribute('width', e.target.naturalWidth) - this.image.setAttribute('height', e.target.naturalHeight) + const image = this.querySelector('img') + + image.addEventListener('load', (e) => { + image.setAttribute('width', e.target.naturalWidth) + image.setAttribute('height', e.target.naturalHeight) this.style.aspectRatio = `${e.target.naturalWidth} / ${e.target.naturalHeight}` }) } - - get image () { - return this.querySelector('img') - } } diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue index bc5c255b..e5560800 100644 --- a/app/javascript/submission_form/area.vue +++ b/app/javascript/submission_form/area.vue @@ -56,11 +56,11 @@
<%= text %>
+ <%== HighlightCode.call(text, 'Shell', theme: 'base16.dark') %>
<%= text %>
+ <%== HighlightCode.call(text, 'Shell', theme: 'base16.dark') %>
@@ -101,7 +101,7 @@
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
- <%= text %>
+ <%== HighlightCode.call(text, 'Shell', theme: 'base16.dark') %>
diff --git a/app/views/icons/_arrow_left.html.erb b/app/views/icons/_arrow_left.html.erb
new file mode 100644
index 00000000..6357e74c
--- /dev/null
+++ b/app/views/icons/_arrow_left.html.erb
@@ -0,0 +1,3 @@
+
diff --git a/app/views/icons/_circle_arrow_left.html.erb b/app/views/icons/_circle_arrow_left.html.erb
new file mode 100644
index 00000000..4b786c4a
--- /dev/null
+++ b/app/views/icons/_circle_arrow_left.html.erb
@@ -0,0 +1,3 @@
+
diff --git a/app/views/icons/_circle_chevron_left.html.erb b/app/views/icons/_circle_chevron_left.html.erb
new file mode 100644
index 00000000..3d12fb49
--- /dev/null
+++ b/app/views/icons/_circle_chevron_left.html.erb
@@ -0,0 +1,3 @@
+
diff --git a/app/views/icons/_folder_plus.html.erb b/app/views/icons/_folder_plus.html.erb
new file mode 100644
index 00000000..4dcc18d1
--- /dev/null
+++ b/app/views/icons/_folder_plus.html.erb
@@ -0,0 +1,3 @@
+
diff --git a/app/views/icons/_slash.html.erb b/app/views/icons/_slash.html.erb
new file mode 100644
index 00000000..4e416c0a
--- /dev/null
+++ b/app/views/icons/_slash.html.erb
@@ -0,0 +1,3 @@
+
diff --git a/app/views/shared/_github.html.erb b/app/views/shared/_github.html.erb
index 49e1f380..df415db6 100644
--- a/app/views/shared/_github.html.erb
+++ b/app/views/shared/_github.html.erb
@@ -1,5 +1,5 @@
-
+
<%= svg_icon('start', class: 'h-3 w-3') %>
9k
diff --git a/app/views/shared/_settings_nav.html.erb b/app/views/shared/_settings_nav.html.erb
index 8355f543..32964656 100644
--- a/app/views/shared/_settings_nav.html.erb
+++ b/app/views/shared/_settings_nav.html.erb
@@ -1,7 +1,7 @@