diff --git a/app/controllers/api/template_folders_autocomplete_controller.rb b/app/controllers/api/template_folders_autocomplete_controller.rb new file mode 100644 index 00000000..19365e76 --- /dev/null +++ b/app/controllers/api/template_folders_autocomplete_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Api + class TemplateFoldersAutocompleteController < ApiBaseController + load_and_authorize_resource :template_folder, parent: false + + LIMIT = 100 + + def index + template_folders = @template_folders.joins(:templates).where(templates: { deleted_at: nil }).distinct + template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT) + + render json: template_folders.as_json(only: %i[name deleted_at]) + end + end +end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index fe7c1c5b..3f9eff1d 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -6,17 +6,56 @@ class DashboardController < ApplicationController before_action :maybe_redirect_product_url before_action :maybe_render_landing + load_and_authorize_resource :template_folder, parent: false load_and_authorize_resource :template, parent: false + SHOW_TEMPLATES_FOLDERS_THRESHOLD = 9 + TEMPLATES_PER_PAGE = 12 + FOLDERS_PER_PAGE = 18 + def index - @templates = @templates.active.preload(:author).order(id: :desc) - @templates = Templates.search(@templates, params[:q]) + @template_folders = filter_template_folders(@template_folders) + + @pagy, @template_folders = pagy( + @template_folders, + items: FOLDERS_PER_PAGE, + page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1 + ) + + if @pagy.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD + @templates = @templates.none + else + @template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME } + @templates = filter_templates(@templates) - @pagy, @templates = pagy(@templates, items: 12) + items = + if @template_folders.size < 4 + TEMPLATES_PER_PAGE + else + (@template_folders.size < 7 ? 9 : 6) + end + + @pagy, @templates = pagy(@templates, items:) + end end private + def filter_template_folders(template_folders) + rel = template_folders.joins(:active_templates) + .order(id: :desc) + .distinct + + TemplateFolders.search(rel, params[:q]) + end + + def filter_templates(templates) + rel = templates.active.preload(:author).order(id: :desc) + .where(folder_id: current_account.default_template_folder.id) + + Templates.search(rel, params[:q]) + end + def maybe_redirect_product_url return if !Docuseal.multitenant? || signed_in? diff --git a/app/controllers/template_folders_controller.rb b/app/controllers/template_folders_controller.rb new file mode 100644 index 00000000..80b81632 --- /dev/null +++ b/app/controllers/template_folders_controller.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class TemplateFoldersController < ApplicationController + load_and_authorize_resource :template_folder + + def show + @templates = @template_folder.templates.active.preload(:author).order(id: :desc) + @templates = Templates.search(@templates, params[:q]) + + @pagy, @templates = pagy(@templates, items: 12) + end + + def edit; end + + def update + if @template_folder != current_account.default_template_folder && + @template_folder.update(template_folder_params) + redirect_to folder_path(@template_folder), notice: 'Folder name has been updated' + else + redirect_to folder_path(@template_folder), alert: 'Unable to rename folder' + end + end + + private + + def template_folder_params + params.require(:template_folder).permit(:name) + end +end diff --git a/app/controllers/templates_archived_controller.rb b/app/controllers/templates_archived_controller.rb index f598b817..82f286d0 100644 --- a/app/controllers/templates_archived_controller.rb +++ b/app/controllers/templates_archived_controller.rb @@ -4,7 +4,7 @@ class TemplatesArchivedController < ApplicationController load_and_authorize_resource :template, parent: false def index - @templates = @templates.where.not(deleted_at: nil).preload(:author).order(id: :desc) + @templates = @templates.where.not(deleted_at: nil).preload(:author, :folder).order(id: :desc) @templates = Templates.search(@templates, params[:q]) @pagy, @templates = pagy(@templates, items: 12) diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index 5d9a7e27..69d0781a 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -31,6 +31,7 @@ class TemplatesController < ApplicationController def create @template.account = current_account @template.author = current_user + @template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name]) @template.assign_attributes(@base_template.slice(:fields, :schema, :submitters)) if @base_template if @template.save diff --git a/app/controllers/templates_folders_controller.rb b/app/controllers/templates_folders_controller.rb new file mode 100644 index 00000000..70b6e341 --- /dev/null +++ b/app/controllers/templates_folders_controller.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class TemplatesFoldersController < ApplicationController + load_and_authorize_resource :template + + def edit; end + + def update + @template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:name]) + + if @template.save + redirect_back(fallback_location: template_path(@template), notice: 'Document template has been moved') + else + redirect_back(fallback_location: template_path(@template), notice: 'Unable to move template into folder') + end + end + + private + + def template_folder_params + params.require(:template_folder).permit(:name) + end +end diff --git a/app/controllers/templates_uploads_controller.rb b/app/controllers/templates_uploads_controller.rb index 1a111dee..07d0e4d2 100644 --- a/app/controllers/templates_uploads_controller.rb +++ b/app/controllers/templates_uploads_controller.rb @@ -6,6 +6,7 @@ class TemplatesUploadsController < ApplicationController def create @template.account = current_account @template.author = current_user + @template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name]) @template.name = File.basename(params[:files].first.original_filename, '.*') @template.save! diff --git a/app/javascript/application.js b/app/javascript/application.js index f9d1e043..0bc13551 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -16,6 +16,7 @@ import SetOriginUrl from './elements/set_origin_url' import SetTimezone from './elements/set_timezone' import AutoresizeTextarea from './elements/autoresize_textarea' import SubmittersAutocomplete from './elements/submitter_autocomplete' +import FolderAutocomplete from './elements/folder_autocomplete' import * as TurboInstantClick from './lib/turbo_instant_click' @@ -45,6 +46,7 @@ window.customElements.define('set-origin-url', SetOriginUrl) window.customElements.define('set-timezone', SetTimezone) window.customElements.define('autoresize-textarea', AutoresizeTextarea) window.customElements.define('submitters-autocomplete', SubmittersAutocomplete) +window.customElements.define('folder-autocomplete', FolderAutocomplete) document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody) document.addEventListener('turbo:submit-end', async (event) => { diff --git a/app/javascript/elements/folder_autocomplete.js b/app/javascript/elements/folder_autocomplete.js new file mode 100644 index 00000000..3d0bb4e3 --- /dev/null +++ b/app/javascript/elements/folder_autocomplete.js @@ -0,0 +1,43 @@ +import autocomplete from 'autocompleter' + +export default class extends HTMLElement { + connectedCallback () { + autocomplete({ + input: this.input, + preventSubmit: this.dataset.submitOnSelect === 'true' ? 0 : 1, + minLength: 0, + showOnFocus: true, + onSelect: this.onSelect, + render: this.render, + fetch: this.fetch + }) + } + + onSelect = (item) => { + this.input.value = item.name + } + + fetch = (text, resolve) => { + const queryParams = new URLSearchParams({ q: text }) + + fetch('/api/template_folders_autocomplete?' + queryParams).then(async (resp) => { + const items = await resp.json() + + resolve(items) + }).catch(() => { + resolve([]) + }) + } + + render = (item) => { + const div = document.createElement('div') + + div.textContent = item.name + + return div + } + + get input () { + return this.querySelector('input') + } +} diff --git a/app/models/account.rb b/app/models/account.rb index bec71f3e..a9659b21 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -17,7 +17,7 @@ class Account < ApplicationRecord has_many :account_configs, dependent: :destroy has_many :templates, dependent: :destroy has_many :template_folders, dependent: :destroy - has_one :default_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) }, + has_one :default_template_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) }, class_name: 'TemplateFolder', dependent: :destroy, inverse_of: :account has_many :submissions, through: :templates has_many :submitters, through: :submissions @@ -27,9 +27,8 @@ class Account < ApplicationRecord attribute :timezone, :string, default: 'UTC' attribute :locale, :string, default: 'en-US' - def default_folder - super || template_folders.new(name: TemplateFolder::DEFAULT_NAME, - author_id: users.minimum(:id)) - .tap(&:save!) + def default_template_folder + super || build_default_template_folder(name: TemplateFolder::DEFAULT_NAME, + author_id: users.minimum(:id)).tap(&:save!) end end diff --git a/app/models/template.rb b/app/models/template.rb index 3ef60fa4..2168689d 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -62,6 +62,6 @@ class Template < ApplicationRecord private def maybe_set_default_folder - self.folder ||= account.default_folder + self.folder ||= account.default_template_folder end end diff --git a/app/models/template_folder.rb b/app/models/template_folder.rb index e7fa4e84..c01fe30c 100644 --- a/app/models/template_folder.rb +++ b/app/models/template_folder.rb @@ -28,7 +28,13 @@ class TemplateFolder < ApplicationRecord belongs_to :author, class_name: 'User' belongs_to :account - has_many :templates, dependent: :destroy + has_many :templates, dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder + has_many :active_templates, -> { where(deleted_at: nil) }, + class_name: 'Template', dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder scope :active, -> { where(deleted_at: nil) } + + def default? + name == DEFAULT_NAME + end end diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb index 4887abba..2bccb360 100644 --- a/app/views/dashboard/index.html.erb +++ b/app/views/dashboard/index.html.erb @@ -3,7 +3,7 @@
- <%= svg_icon('calendar', class: 'w-4 h-4') %> - <%= l(template.created_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %> +
+ + <%= svg_icon('calendar', class: 'w-4 h-4') %> + <%= l(template.created_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %> + + <% if template.deleted_at? %> + + <%= svg_icon('folder', class: 'w-4 h-4 flex-shrink-0') %> + <%= template.folder.name %> + + <% end %>