Add chip filters for the new time window filters

rename close => closure to ease interpretation by the LLM
This commit is contained in:
Jorge Manrubia
2025-06-27 11:34:31 +02:00
parent 3391a20843
commit 4d5017b671
29 changed files with 57862 additions and 17 deletions
+8 -7
View File
@@ -49,7 +49,7 @@ class Command::Ai::Translator
"tag_ids": string[],
"creation": "today" | "yesterday" | "thisweek" | "thismonth" | "thisyear"
| "lastweek" | "lastmonth" | "lastyear",
"completion": "today" | "yesterday" | "thisweek" | "thismonth" | "thisyear"
"closure": "today" | "yesterday" | "thisweek" | "thismonth" | "thisyear"
| "lastweek" | "lastmonth" | "lastyear"
},
"commands": string[] /* OPTIONAL, each starts with "/" */
@@ -90,7 +90,7 @@ class Command::Ai::Translator
* tag_ids — array of tag names
* creation — relative range when the card was **created** (values listed above). Use it only
when the user asks for cards created in a specific timeframe.
* completion — relative range when the card was **completed/closed** (values listed above). Use it
* closure — relative range when the card was **completed/closed** (values listed above). Use it
only when the user asks for cards completed/closed in a specific timeframe.
---------------------- EXPLICIT FILTERING RULES ----------------------
@@ -112,7 +112,7 @@ class Command::Ai::Translator
today, yesterday, thisweek, thismonth, thisyear,
lastweek, lastmonth, lastyear** ) → indexed_by: "closed"
Never add "completion" unless one of the eight
Never add "closure" unless one of the eight
timeframe tokens is present in the user text.
* Never add the literal words "card" or "cards" to terms; treat them as
@@ -136,7 +136,7 @@ class Command::Ai::Translator
Only use "latest" if the user mentions activity, updates, or changes
Otherwise, prefer "newest" for generic mentions of “recent”
* "Completed/closed cards" (no date range) → indexed_by: "closed"
VERY IMPORTANT: Do **not** set "completion" filter unless the user explicitly supplies a timeframe
VERY IMPORTANT: Do **not** set "closure" filter unless the user explicitly supplies a timeframe
(e.g., “completed this month”, “closed last week”).
(If the timeframe is supplied with “closed” instead of “completed”, treat it the same way.)
@@ -184,11 +184,12 @@ class Command::Ai::Translator
* Never duplicate the assignee in both commands and context.
If the request says “assign to X”, produce only /assign X, never assignee_ids
* Never add properties tied to UI view ("card", "list", etc.).
* To filter completed or closed cards, use "indexed_by: closed", don't set a "completion" filter unless the user is
* To filter completed or closed cards, use "indexed_by: closed", don't set a "closure" filter unless the user is
asking for cards completed in a certain window of time.
* When you see a word with a # prefix, assume it refers to a tag (either a filter or a command argument, but don't search for it).
* All filters, including terms, must live **inside** context.
* Do not duplicate terms across properties.
* Don't use "creation" and "closure" filters at the same time.
* Avoid redundant terms.
---------------------------- OUTPUT CLEANLINESS ----------------------------
@@ -238,7 +239,7 @@ class Command::Ai::Translator
User: completed cards yesterday
Output:
{
"context": { "indexed_by": "closed", "completion": "yesterday" }
"context": { "indexed_by": "closed", "closure": "yesterday" }
}
User: "cards tagged with #design" or "#design cards"
@@ -269,7 +270,7 @@ class Command::Ai::Translator
User: cards completed last week
Output:
{
"context": { "completion": "lastweek", "indexed_by": "closed" }
"context": { "closure": "lastweek", "indexed_by": "closed" }
}
Fallback search example (when nothing matches):
+2 -2
View File
@@ -20,7 +20,7 @@ class Filter < ApplicationRecord
@cards ||= begin
result = creator.accessible_cards.indexed_by(indexed_by)
result = result.where(id: card_ids) if card_ids.present?
result = result.open unless indexed_by.closed? || close_window || card_ids.present?
result = result.open unless indexed_by.closed? || closure_window || card_ids.present?
result = result.by_engagement_status(engagement_status) if engagement_status.present?
result = result.unassigned if assignment_status.unassigned?
result = result.assigned_to(assignees.ids) if assignees.present?
@@ -29,7 +29,7 @@ class Filter < ApplicationRecord
result = result.in_stage(stages.ids) if stages.present?
result = result.tagged_with(tags.ids) if tags.present?
result = result.where("cards.created_at": creation_window) if creation_window
result = result.closed_at_window(close_window) if close_window
result = result.closed_at_window(closure_window) if closure_window
result = terms.reduce(result) do |result, term|
result.mentioning(term)
end
+3 -3
View File
@@ -17,7 +17,7 @@ module Filter::Fields
included do
store_accessor :fields, :assignment_status, :indexed_by, :terms,
:engagement_status, :card_ids, :creation, :close
:engagement_status, :card_ids, :creation, :closure
def assignment_status
super.to_s.inquiry
@@ -35,8 +35,8 @@ module Filter::Fields
TimeWindowParser.parse(creation)
end
def close_window
TimeWindowParser.parse(close)
def closure_window
TimeWindowParser.parse(closure)
end
def terms
+1 -1
View File
@@ -41,7 +41,7 @@ module Filter::Params
params[:indexed_by] = indexed_by
params[:engagement_status] = engagement_status
params[:creation] = creation
params[:close] = close
params[:closure] = closure
params[:assignment_status] = assignment_status
params[:terms] = terms
params[:tag_ids] = tags.ids
+17
View File
@@ -1,10 +1,27 @@
class TimeWindowParser
attr_reader :now
HUMAN_NAMES_BY_VALUE = {
"today" => "Today",
"yesterday" => "Yesterday",
"thisweek" => "This week",
"thismonth" => "This month",
"thisyear" => "This year",
"lastweek" => "Last week",
"lastmonth" => "Last month",
"lastyear" => "Last year"
}
VALUES = HUMAN_NAMES_BY_VALUE.keys
class << self
def parse(string)
new.parse(string)
end
def human_name_for(value)
HUMAN_NAMES_BY_VALUE[value]
end
end
def initialize(now: Time.current)
+3 -1
View File
@@ -18,10 +18,12 @@
<%= render "filters/tags", filter: filter %>
<%= render "filters/assignees", filter: filter %>
<%= render "filters/creators", filter: filter %>
<%= render "filters/time_window", filter: filter, name: :creation, label: "Created" %>
<%= render "filters/time_window", filter: filter, name: :closure, label: "Closed" %>
<%# FIXME LATER: this needs to be able to allow selection of unique stages across all active workflows %>
<%#= render "filters/stages", filter: filter %>
<%= render "filters/cards", filter: filter %>
<% filter.terms.each do |term| %>
<%= filter_chip_tag %Q("#{term}"), filter.as_params_without(:terms, term) %>
+32
View File
@@ -0,0 +1,32 @@
<% if filter.public_send("#{name}_window").present? %>
<div class="quick-filter position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn input input--select flex-inline txt-x-small" data-action="click->dialog#open:stop">
<span class="overflow-ellipsis">
<%= "#{label} #{TimeWindowParser.human_name_for(filter.public_send(name))&.downcase}" %>
</span>
</button>
<dialog class="events__popup popup panel flex-column align-start gap-half fill-white shadow"
aria-label="Created…" aria-description="Created…"
data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
<strong class="popup__title margin-block-start-half pad-inline-half">Created…</strong>
<%= form_with url: cards_path, method: :get, class: "popup__list",
data: { controller: "form" } do |form| %>
<% filter.as_params.except(name).each do |key, value| %>
<%= filter_hidden_field_tag key, value %>
<% end %>
<% TimeWindowParser::VALUES.each do |value| %>
<div class="btn popup__item">
<%= form.radio_button name, value,
checked: filter.public_send(name) == value,
data: { action: "change->form#submit" } %>
<%= form.label name, TimeWindowParser.human_name_for(value), value: value, class: "overflow-ellipsis" %>
<%= icon_tag "check", class: "checked flex-item-justify-end" %>
</div>
<% end %>
<% end %>
</dialog>
</div>
<% end %>
+1 -1
View File
@@ -122,7 +122,7 @@ class Command::Ai::TranslatorTest < ActionDispatch::IntegrationTest
end
test "filter by time ranges" do
assert_command({context: {completion: "thisweek", indexed_by: "closed"}}, "cards completed this week")
assert_command({context: {closure: "thisweek", indexed_by: "closed"}}, "cards completed this week")
assert_command({context: {creation: "thisweek", tag_ids: ["design"], creator_ids: [ "jz" ]}}, "cards created this week by jz tagged as #design")
end
+2 -2
View File
@@ -144,8 +144,8 @@ class FilterTest < ActiveSupport::TestCase
assert_includes filter.cards, cards(:logo)
end
test "close window" do
filter = users(:david).filters.new close: "this week"
test "closure window" do
filter = users(:david).filters.new closure: "this week"
cards(:shipping).closure.update_columns created_at: 2.weeks.ago
assert_not_includes filter.cards, cards(:shipping)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff