diff --git a/app/javascript/application.js b/app/javascript/application.js index 91f60f003..56c8fbf2a 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -10,6 +10,7 @@ import ToggleCookies from './elements/toggle_cookies' import DisableHidden from './elements/disable_hidden' import TurboModal from './elements/turbo_modal' import NativeAction from './elements/native_action' +import NativeMenu from './elements/native_menu' import NativeSearch from './elements/native_search' import NativeModal from './elements/native_modal' import NativeEvent from './elements/native_event' @@ -181,6 +182,7 @@ safeRegisterElement('toggle-visible', ToggleVisible) safeRegisterElement('disable-hidden', DisableHidden) safeRegisterElement('turbo-modal', TurboModal) safeRegisterElement('native-action', NativeAction) +safeRegisterElement('native-menu', NativeMenu) safeRegisterElement('native-search', NativeSearch) safeRegisterElement('native-modal', NativeModal) safeRegisterElement('native-event', NativeEvent) diff --git a/app/javascript/elements/native_menu.js b/app/javascript/elements/native_menu.js new file mode 100644 index 000000000..95842bdfc --- /dev/null +++ b/app/javascript/elements/native_menu.js @@ -0,0 +1,48 @@ +let counter = 0 + +export default class extends HTMLElement { + connectedCallback () { + if (window.webkit?.messageHandlers?.native) { + this.addEventListener('click', this.onClick) + } else { + document.addEventListener('pointerdown', this.onPointerDown) + } + } + + disconnectedCallback () { + this.removeEventListener('click', this.onClick) + document.removeEventListener('pointerdown', this.onPointerDown) + } + + onPointerDown = (event) => { + if (this.contains(document.activeElement) && !this.contains(event.target)) { + document.activeElement.blur() + } + } + + onClick = (event) => { + if (event.target.closest('native-menu-action')) return + + const rect = this.firstElementChild.getBoundingClientRect() + + const items = [...this.querySelectorAll('native-menu-action')].map((action) => { + const target = action.querySelector('a, button, [role="button"]') || action.firstElementChild + + target.dataset.nativeId ||= `native-menu-${++counter}` + + return { + id: target.dataset.nativeId, + title: action.dataset.label || target.textContent.trim(), + icon: action.dataset.icon, + destructive: action.dataset.destructive === 'true', + selected: action.dataset.selected === 'true' + } + }) + + window.webkit.messageHandlers.native.postMessage({ + type: 'menu', + rect: { x: rect.left + window.scrollX, y: rect.top + window.scrollY, width: rect.width, height: rect.height }, + items + }) + } +} diff --git a/app/views/icons/_dots.html.erb b/app/views/icons/_dots.html.erb new file mode 100644 index 000000000..8ffd09f7e --- /dev/null +++ b/app/views/icons/_dots.html.erb @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/views/submissions_filters/_applied_filters.html.erb b/app/views/submissions_filters/_applied_filters.html.erb index c83770b0a..63fcf4b92 100644 --- a/app/views/submissions_filters/_applied_filters.html.erb +++ b/app/views/submissions_filters/_applied_filters.html.erb @@ -11,7 +11,7 @@ <% status_chip_html = capture do %> <% if status_icon || default_status_icon %>
- <%= link_to submissions_filter_path('status', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal', native_modal: 'medium' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %> + <%= link_to submissions_filter_path('status', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal', native_modal: 'medium', native_title: t('status') }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %> <%= svg_icon(status_icon || default_status_icon, class: 'w-5 h-5 shrink-0') %> <%= t(params[:status]) %> <% end %> @@ -46,7 +46,7 @@ <% end %> <% if params[:folder].present? %>
- <%= link_to submissions_filter_path('folder', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal', native_modal: 'medium' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %> + <%= link_to submissions_filter_path('folder', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal', native_modal: 'medium', native_title: t('folder') }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %> <%= svg_icon('folder', class: 'w-5 h-5 shrink-0') %> <%= params[:folder] %> <% end %> @@ -57,7 +57,7 @@ <% end %> <% if params[:author].present? %>
- <%= link_to submissions_filter_path('author', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal', native_modal: 'medium' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %> + <%= link_to submissions_filter_path('author', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal', native_modal: 'medium', native_title: t('author') }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %> <%= svg_icon('user', class: 'w-5 h-5 shrink-0') %> <%= current_account.users.accessible_by(current_ability).where(account: current_account).find_by(email: params[:author])&.full_name || 'NA' %> <% end %> @@ -68,7 +68,7 @@ <% end %> <% if query_params[:completed_at_from].present? || query_params[:completed_at_to].present? %>
- <%= link_to submissions_filter_path('completed_at', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal', native_modal: 'medium' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %> + <%= link_to submissions_filter_path('completed_at', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal', native_modal: 'medium', native_title: t('completed_at') }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %> <%= svg_icon('calendar_check', class: 'w-5 h-5 shrink-0') %> <% if query_params[:completed_at_from] == query_params[:completed_at_to] %> @@ -86,7 +86,7 @@ <% end %> <% if query_params[:created_at_from].present? || query_params[:created_at_to].present? %>
- <%= link_to submissions_filter_path('created_at', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal', native_modal: 'medium' }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %> + <%= link_to submissions_filter_path('created_at', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal', native_modal: 'medium', native_title: t('created_at') }, class: 'flex items-center space-x-1 flex-1 min-w-0 pr-1' do %> <%= svg_icon('calendar', class: 'w-5 h-5 shrink-0') %> <% if query_params[:created_at_from] == query_params[:created_at_to] %> @@ -105,53 +105,59 @@ <% end %> <% with_status_dropdown = with_status_button && chips_html.blank? %> <% if with_status_dropdown %> - + <% end %> <% if chips_html.present? || status_chip_html.present? %> <% if with_status_button && !with_status_dropdown %> - + <%= svg_icon(status_icons[current_status], class: 'w-5 h-5 shrink-0') %> <%= t(current_status) %> <%= svg_icon('chevron_down', class: 'w-4 h-4 shrink-0') %> diff --git a/app/views/submissions_filters/_filter_button.html.erb b/app/views/submissions_filters/_filter_button.html.erb index c485080cb..07a07c7f9 100644 --- a/app/views/submissions_filters/_filter_button.html.erb +++ b/app/views/submissions_filters/_filter_button.html.erb @@ -1,42 +1,52 @@ <% query_params = params.permit(:q, :scope, :archived).merge(filter_params) %> <% advanced_filters = filter_params.then { |f| f[:status].in?(%w[pending completed]) ? f.except(:status) : f } %> - + diff --git a/app/views/templates/_template.html.erb b/app/views/templates/_template.html.erb index 936de7e85..c05b1208f 100644 --- a/app/views/templates/_template.html.erb +++ b/app/views/templates/_template.html.erb @@ -68,4 +68,53 @@ <% end %>
+ <% if !template.archived_at? && (can?(:update, template) || can?(:create, template) || can?(:destroy, template)) %> + + + + + <% end %>