Support user prompts in Fizzy do, move parsing logic to context
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
class Prompts::UsersController < ApplicationController
|
||||
def index
|
||||
@users = User.all
|
||||
|
||||
if stale? etag: @users
|
||||
render layout: false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,10 @@ module RichTextHelper
|
||||
content_tag "lexical-prompt", "", trigger: "@", src: prompts_collection_users_path(collection), name: "mention"
|
||||
end
|
||||
|
||||
def global_mentions_prompt
|
||||
content_tag "lexical-prompt", "", trigger: "@", src: prompts_users_path, name: "mention"
|
||||
end
|
||||
|
||||
def cards_prompt
|
||||
content_tag "lexical-prompt", "", trigger: "#", src: prompts_cards_path, name: "card", "insert-editable-text": true, "remote-filtering": true
|
||||
end
|
||||
|
||||
@@ -41,9 +41,9 @@ export default class extends Controller {
|
||||
this.#hideOutput()
|
||||
}
|
||||
|
||||
submitCommand() {
|
||||
this.formTarget.requestSubmit()
|
||||
this.#reset()
|
||||
submitCommand({ target }) {
|
||||
console.debug("Es", target);
|
||||
this.#submitCommand()
|
||||
}
|
||||
|
||||
handleKeyPress(event) {
|
||||
@@ -183,7 +183,12 @@ export default class extends Controller {
|
||||
this.inputTarget.value = this.originalInputValue
|
||||
this.confirmationTarget.value = "confirmed"
|
||||
this.#hideOutput()
|
||||
this.submitCommand()
|
||||
this.#submitCommand()
|
||||
}
|
||||
|
||||
#submitCommand() {
|
||||
this.formTarget.requestSubmit()
|
||||
this.#reset()
|
||||
}
|
||||
|
||||
#showOutput(markdown) {
|
||||
|
||||
@@ -42,10 +42,10 @@ class Command::Ai::Parser
|
||||
def resolve_named_params_to_ids(normalized_query)
|
||||
normalized_query.tap do |query_json|
|
||||
if query_context = query_json[:context].presence
|
||||
query_context[:assignee_ids] = query_context[:assignee_ids]&.filter_map { |name| assignee_from(name)&.id }
|
||||
query_context[:creator_ids] = query_context[:creator_ids]&.filter_map { |name| assignee_from(name)&.id }
|
||||
query_context[:collection_ids] = query_context[:collection_ids]&.filter_map { |name| Collection.where("lower(name) like ?", "%#{name.downcase}%").first&.id }
|
||||
query_context[:tag_ids] = query_context[:tag_ids]&.filter_map { |name| ::Tag.find_by_title(name)&.id }
|
||||
query_context[:assignee_ids] = query_context[:assignee_ids]&.filter_map { |name| context.find_user(name)&.id }
|
||||
query_context[:creator_ids] = query_context[:creator_ids]&.filter_map { |name| context.find_user(name)&.id }
|
||||
query_context[:collection_ids] = query_context[:collection_ids]&.filter_map { |name| context.find_collection(name)&.id }
|
||||
query_context[:tag_ids] = query_context[:tag_ids]&.filter_map { |name| context.find_tag(name)&.id }
|
||||
query_context.compact!
|
||||
end
|
||||
end
|
||||
|
||||
@@ -145,8 +145,10 @@ class Command::Ai::Translator
|
||||
* If cards are described as state ("assigned to X") and later an action ("assign X"), only the first is a filter.
|
||||
* ❗ Once you produce a valid context **or** command list, do not add a fallback /search.
|
||||
|
||||
-------------------- COMMAND INTERPRETATION RULES --------------------
|
||||
---------------------- RESOLVE COMMAND ARGUMENTS ----------------------
|
||||
* A person can be express by its name or via a global ID URL like gid://fizzy/User/773524000?tenant=37signals.
|
||||
|
||||
-------------------- COMMAND INTERPRETATION RULES --------------------
|
||||
* /user <Name> → open that person’s profile or activity feed.
|
||||
– Phrases like “visit user <Name>”, “view user <Name>”, “see <Name>’s profile” must map to **/user**, **never** to /visit.
|
||||
* /visit <url|path> → open any other URL or internal path (cards, settings, etc.).
|
||||
|
||||
@@ -8,14 +8,24 @@ class Command::Parser
|
||||
end
|
||||
|
||||
def parse(string)
|
||||
parse_command(string).tap do |command|
|
||||
parseable_command = as_plain_text_with_attachable_references(string)
|
||||
|
||||
parse_command(parseable_command).tap do |command|
|
||||
command.user = user
|
||||
command.line ||= string
|
||||
command.line ||= as_plain_text(string)
|
||||
command.context ||= context
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def as_plain_text_with_attachable_references(string)
|
||||
ActionText::Content.new(string).render_attachments(&:to_gid).fragment.to_plain_text
|
||||
end
|
||||
|
||||
def as_plain_text(string)
|
||||
ActionText::Content.new(string).to_plain_text
|
||||
end
|
||||
|
||||
def parse_command(string)
|
||||
command_name, *command_arguments = string.strip.split(" ")
|
||||
combined_arguments = command_arguments.join(" ")
|
||||
@@ -24,9 +34,9 @@ class Command::Parser
|
||||
when /^#/
|
||||
Command::FilterByTag.new(tag_title: tag_title_from(string), params: filter.as_params)
|
||||
when /^@/
|
||||
Command::GoToUser.new(user_id: assignee_from(command_name)&.id)
|
||||
Command::GoToUser.new(user_id: context.find_user(command_name)&.id)
|
||||
when "/user"
|
||||
Command::GoToUser.new(user_id: assignee_from(combined_arguments)&.id)
|
||||
Command::GoToUser.new(user_id: context.find_user(combined_arguments)&.id)
|
||||
when "/assign", "/assignto"
|
||||
Command::Assign.new(assignee_ids: assignees_from(command_arguments).collect(&:id), card_ids: cards.ids)
|
||||
when "/clear"
|
||||
@@ -44,7 +54,7 @@ class Command::Parser
|
||||
when "/search"
|
||||
Command::Search.new(terms: combined_arguments)
|
||||
when "/stage"
|
||||
Command::Stage.new(stage_id: stage_from(combined_arguments)&.id, card_ids: cards.ids)
|
||||
Command::Stage.new(stage_id: context.find_workflow_stage(combined_arguments)&.id, card_ids: cards.ids)
|
||||
when "/visit"
|
||||
Command::VisitUrl.new(url: command_arguments.first)
|
||||
when "/tag"
|
||||
@@ -57,20 +67,7 @@ class Command::Parser
|
||||
private
|
||||
def assignees_from(strings)
|
||||
Array(strings).filter_map do |string|
|
||||
assignee_from(string)
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: This is temporary as it can be ambiguous. We should inject the user ID in the command
|
||||
# under the hood instead, as determined by the user picker. E.g: @1234.
|
||||
def assignee_from(string)
|
||||
string_without_at = string.delete_prefix("@")
|
||||
User.all.find { |user| user.mentionable_handles.include?(string_without_at.downcase) }
|
||||
end
|
||||
|
||||
def stage_from(combined_arguments)
|
||||
candidate_stages.find do |stage|
|
||||
stage.name.downcase.include?(combined_arguments.downcase)
|
||||
context.find_user(string)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -78,10 +75,6 @@ class Command::Parser
|
||||
cards.first&.collection || Collection.first
|
||||
end
|
||||
|
||||
def candidate_stages
|
||||
Workflow::Stage.where(workflow_id: cards.joins(:collection).select("collections.workflow_id").distinct)
|
||||
end
|
||||
|
||||
def tag_title_from(string)
|
||||
string.gsub(/^#/, "")
|
||||
end
|
||||
|
||||
@@ -28,6 +28,29 @@ class Command::Parser::Context
|
||||
viewing_cards_index? || viewing_search_results?
|
||||
end
|
||||
|
||||
def find_user(string)
|
||||
if string.starts_with?("gid://")
|
||||
User.find_by_id(GlobalID::Locator.locate(string).id)
|
||||
else
|
||||
string_without_at = string.delete_prefix("@")
|
||||
User.all.find { |user| user.mentionable_handles.include?(string_without_at.downcase) }
|
||||
end
|
||||
end
|
||||
|
||||
def find_workflow_stage(string)
|
||||
candidate_stages.find do |stage|
|
||||
stage.name.downcase.include?(combined_arguments.downcase)
|
||||
end
|
||||
end
|
||||
|
||||
def find_tag(string)
|
||||
Tag.find_by_title(string)
|
||||
end
|
||||
|
||||
def find_collection(string)
|
||||
Collection.where("lower(name) like ?", "%#{name.downcase}%").first
|
||||
end
|
||||
|
||||
private
|
||||
attr_reader :controller, :action, :params
|
||||
|
||||
@@ -57,4 +80,8 @@ class Command::Parser::Context
|
||||
@action = route[:action]
|
||||
@params = ActionController::Parameters.new(Rack::Utils.parse_nested_query(uri.query).merge(route.except(:controller, :action)))
|
||||
end
|
||||
|
||||
def candidate_stages
|
||||
Workflow::Stage.where(workflow_id: cards.joins(:collection).select("collections.workflow_id").distinct)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,6 +26,11 @@ class User < ApplicationRecord
|
||||
update! active: false, email_address: deactived_email_address
|
||||
end
|
||||
|
||||
# TODO: Move to attachable along with the concern
|
||||
def attachable_plain_text_representation(...)
|
||||
"@#{first_name.downcase}"
|
||||
end
|
||||
|
||||
private
|
||||
def deactived_email_address
|
||||
email_address.sub(/@/, "-deactivated-#{SecureRandom.uuid}@")
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
action: "keydown.enter->terminal#submitCommand keydown.up->toggle-class#add:prevent keydown.up->navigable-list#selectCurrentOrReset:stop terminal#hideError"
|
||||
},
|
||||
placeholder: platform.desktop? ? "Press #{ hotkey_label(["ctrl", "K"]) } to search or type commands…" : "Search or type commands…",
|
||||
spellcheck: "false" %>
|
||||
spellcheck: "false" do %>
|
||||
<%= global_mentions_prompt %>
|
||||
<% end %>
|
||||
|
||||
<%= hidden_field_tag "confirmed", nil, data: { terminal_target: "confirmation" } %>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<%= render partial: "prompts/collections/users/user", collection: @users %>
|
||||
Reference in New Issue
Block a user