Add support for filtering by creation or completion windows of times
This commit is contained in:
@@ -8,6 +8,7 @@ module Card::Closeable
|
|||||||
scope :open, -> { where.missing(:closure) }
|
scope :open, -> { where.missing(:closure) }
|
||||||
|
|
||||||
scope :recently_closed_first, -> { closed.order("closures.created_at": :desc) }
|
scope :recently_closed_first, -> { closed.order("closures.created_at": :desc) }
|
||||||
|
scope :closed_at_window, ->(window) { closed.where("closures.created_at": window) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def closed?
|
def closed?
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ class Command::Ai::Translator
|
|||||||
def prompt
|
def prompt
|
||||||
<<~PROMPT
|
<<~PROMPT
|
||||||
You are Fizzy’s command translator.
|
You are Fizzy’s command translator.
|
||||||
|
|
||||||
--------------------------- OUTPUT FORMAT ---------------------------
|
--------------------------- OUTPUT FORMAT ---------------------------
|
||||||
Return ONE valid JSON object matching **exactly**:
|
Return ONE valid JSON object matching **exactly**:
|
||||||
|
|
||||||
{
|
{
|
||||||
"context": { /* REQUIRED unless empty */
|
"context": { /* REQUIRED unless empty */
|
||||||
"terms": string[],
|
"terms": string[],
|
||||||
@@ -46,35 +46,39 @@ class Command::Ai::Translator
|
|||||||
"card_ids": number[],
|
"card_ids": number[],
|
||||||
"creator_id": string,
|
"creator_id": string,
|
||||||
"collection_ids": string[],
|
"collection_ids": string[],
|
||||||
"tag_ids": string[]
|
"tag_ids": string[],
|
||||||
|
"creation": "today" | "yesterday" | "thisweek" | "thismonth" | "thisyear"
|
||||||
|
| "lastweek" | "lastmonth" | "lastyear",
|
||||||
|
"completion": "today" | "yesterday" | "thisweek" | "thismonth" | "thisyear"
|
||||||
|
| "lastweek" | "lastmonth" | "lastyear"
|
||||||
},
|
},
|
||||||
"commands": string[] /* OPTIONAL, each starts with "/" */
|
"commands": string[] /* OPTIONAL, each starts with "/" */
|
||||||
}
|
}
|
||||||
|
|
||||||
❗ If any filter key appears outside "context", the response is **INVALID**.
|
❗ If any filter key appears outside "context", the response is **INVALID**.
|
||||||
|
|
||||||
If neither context nor commands is appropriate, output **exactly**:
|
If neither context nor commands is appropriate, output **exactly**:
|
||||||
{ "commands": ["/search <user request>"] }
|
{ "commands": ["/search <user request>"] }
|
||||||
|
|
||||||
– Do NOT add any other top-level keys.
|
– Do NOT add any other top-level keys.
|
||||||
– Responses must be valid JSON (no comments, no trailing commas, no extra text).
|
– Responses must be valid JSON (no comments, no trailing commas, no extra text).
|
||||||
|
|
||||||
----------------------- INTERNAL THINKING STEPS ----------------------
|
----------------------- INTERNAL THINKING STEPS ----------------------
|
||||||
(Do **not** output these steps.)
|
(Do **not** output these steps.)
|
||||||
|
|
||||||
1. Decide whether the user’s request
|
1. Decide whether the user’s request
|
||||||
a. only filters existing cards → fill context
|
a. only filters existing cards → fill context
|
||||||
b. requires actions → add commands in spoken order
|
b. requires actions → add commands in spoken order
|
||||||
c. matches neither → fallback search
|
c. matches neither → fallback search
|
||||||
2. Emit the FizzyOutput object.
|
2. Emit the FizzyOutput object.
|
||||||
|
|
||||||
------------------ DOMAIN KNOWLEDGE & INTERPRETATION -----------------
|
------------------ DOMAIN KNOWLEDGE & INTERPRETATION -----------------
|
||||||
Cards represent issues, features, bugs, tasks, or problems.
|
Cards represent issues, features, bugs, tasks, or problems.
|
||||||
Cards have comments and live inside collections.
|
Cards have comments and live inside collections.
|
||||||
|
|
||||||
Context filters describe card state already true.
|
Context filters describe card state already true.
|
||||||
Commands (/assign, /tag, /close, /search, /clear, /do, /consider, /stage, /visit, /add_card) apply new actions.
|
Commands (/assign, /tag, /close, /search, /clear, /do, /consider, /stage, /visit, /add_card) apply new actions.
|
||||||
|
|
||||||
Context properties you may use
|
Context properties you may use
|
||||||
* terms — array of keywords
|
* terms — array of keywords
|
||||||
* indexed_by — "newest", "oldest", "latest", "stalled", "closed"
|
* indexed_by — "newest", "oldest", "latest", "stalled", "closed"
|
||||||
@@ -84,24 +88,38 @@ class Command::Ai::Translator
|
|||||||
* creator_id — creator’s name
|
* creator_id — creator’s name
|
||||||
* collection_ids — array of collections
|
* collection_ids — array of collections
|
||||||
* tag_ids — array of tag names
|
* 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
|
||||||
|
only when the user asks for cards completed/closed in a specific timeframe.
|
||||||
|
|
||||||
---------------------- EXPLICIT FILTERING RULES ----------------------
|
---------------------- EXPLICIT FILTERING RULES ----------------------
|
||||||
|
|
||||||
* Use terms only if the query explicitly refers to cards; plain-text searches go to /search.
|
* Use terms only if the query explicitly refers to cards; plain-text searches go to /search.
|
||||||
* Numbers without the word "card(s)" default to terms **unless the number is the direct object of an
|
* Numbers without the word "card(s)" default to terms **unless the number is the direct object of an
|
||||||
action verb that operates on cards (move, assign, tag, close, stage, consider, do, etc.).**
|
action verb that operates on cards (move, assign, tag, close, stage, consider, do, etc.).**
|
||||||
– "123" → terms: ["123"]
|
– "123" (with no action verb) → context: { terms: ["123"] }
|
||||||
– "card 123" → card_id: ["123"]
|
– "card 123" → context: { card_ids: [123] }
|
||||||
– "card 1,2" → card_ids: [1, 2]
|
– "card 1,2" → context: { card_ids: [1, 2] }
|
||||||
– "move 1 and 2 to doing" → context.card_ids = [1, 2]; command /do
|
– "move 1 and 2 to doing" → context: { card_ids: [1, 2] }, commands: ["/do"]
|
||||||
– "123" (with no action verb) → terms: ["123"]
|
|
||||||
* Quick mnemonic
|
Quick mnemonic
|
||||||
WORD “card(s)” present? → card_ids
|
WORD “card(s)” present? → card_ids
|
||||||
ACTION verb present? → card_ids + command
|
ACTION verb present? → card_ids + command
|
||||||
Otherwise → terms
|
Otherwise → terms
|
||||||
|
|
||||||
|
* "Completed/closed cards" ( **and NO words like
|
||||||
|
today, yesterday, thisweek, thismonth, thisyear,
|
||||||
|
lastweek, lastmonth, lastyear** ) → indexed_by: "closed"
|
||||||
|
|
||||||
|
– Never add "completion" 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
|
||||||
|
stop-words that simply introduce the query scope.
|
||||||
* "X collection" → collection_ids: ["X"]
|
* "X collection" → collection_ids: ["X"]
|
||||||
* **Past-tense** “assigned to X” → assignee_ids: ["X"] (filter)
|
* **Past-tense** “assigned to X” → assignee_ids: ["X"] (filter)
|
||||||
* **Imperative** “assign to X”, “assign to me” → command /assign X
|
* **Imperative** “assign to X”, “assign to me” → command /assign X
|
||||||
– Never use assignee_ids when the user gives an imperative assignment
|
– Never use assignee_ids when the user gives an imperative assignment
|
||||||
* "Created by X" → creator_id: "X"
|
* "Created by X" → creator_id: "X"
|
||||||
* "Stagnated or stalled cards" → indexed_by: "stalled"
|
* "Stagnated or stalled cards" → indexed_by: "stalled"
|
||||||
@@ -112,20 +130,24 @@ class Command::Ai::Translator
|
|||||||
→ assignment_status: "unassigned".
|
→ assignment_status: "unassigned".
|
||||||
– IMPORTANT: Only set assignment_status when the user **explicitly** asks for an unassigned state
|
– IMPORTANT: Only set assignment_status when the user **explicitly** asks for an unassigned state
|
||||||
– Do NOT infer unassigned just because an assignment follows
|
– Do NOT infer unassigned just because an assignment follows
|
||||||
– “Assign to David” → /assign david (do NOT include assignment_status)
|
|
||||||
* "My cards" → assignee_ids of requester (if identifiable)
|
* "My cards" → assignee_ids of requester (if identifiable)
|
||||||
* “Recent cards” (i.e., newly created) → indexed_by: "newest"
|
* “Recent cards” (i.e., newly created) → indexed_by: "newest"
|
||||||
* “Cards with recent activity”, “recently updated cards” → indexed_by: "latest"
|
* “Cards with recent activity”, “recently updated cards” → indexed_by: "latest"
|
||||||
– Only use "latest" if the user mentions activity, updates, or changes
|
– Only use "latest" if the user mentions activity, updates, or changes
|
||||||
– Otherwise, prefer "newest" for generic mentions of “recent”
|
– Otherwise, prefer "newest" for generic mentions of “recent”
|
||||||
* "Completed/closed cards" → indexed_by: "closed"
|
* "Completed/closed cards" (no date range) → indexed_by: "closed"
|
||||||
|
– VERY IMPORTANT: Do **not** set "completion" 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.)
|
||||||
|
|
||||||
* If cards are described as state ("assigned to X") and later an action ("assign X"), only the first is a filter.
|
* 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.
|
* ❗ Once you produce a valid context **or** command list, do not add a fallback /search.
|
||||||
|
|
||||||
-------------------- COMMAND INTERPRETATION RULES --------------------
|
-------------------- COMMAND INTERPRETATION RULES --------------------
|
||||||
|
|
||||||
* /do → engage with card and move it to "doing"
|
* /do → engage with card and move it to "doing"
|
||||||
* /consider → move card back to "considering" (reconsider)
|
* /consider → move card back to "considering" (reconsider)
|
||||||
* Unless a clear command applies, fallback to /search with the verbatim text.
|
* Unless a clear command applies, fallback to /search with the verbatim text.
|
||||||
* When searching for nouns (non-person), prefer /search over terms.
|
* When searching for nouns (non-person), prefer /search over terms.
|
||||||
* Respect the spoken order of commands.
|
* Respect the spoken order of commands.
|
||||||
@@ -142,89 +164,115 @@ class Command::Ai::Translator
|
|||||||
* “Move <ID(s)> to <Stage>” → context.card_ids = [IDs]; command /stage <Stage>
|
* “Move <ID(s)> to <Stage>” → context.card_ids = [IDs]; command /stage <Stage>
|
||||||
* /add_card → Create a new card with a blank title
|
* /add_card → Create a new card with a blank title
|
||||||
* /add_card [title] → Create a new card with the provided title
|
* /add_card [title] → Create a new card with the provided title
|
||||||
|
|
||||||
---------------------------- VISIT SCREENS ---------------------------
|
---------------------------- VISIT SCREENS ---------------------------
|
||||||
|
|
||||||
You can open these screens by using /visits with their urls:
|
You can open these screens by using /visit with their urls:
|
||||||
|
|
||||||
* My profile → /visit #{user_path(user)}
|
* My profile → /visit #{user_path(user)}
|
||||||
* Edit my profile (including your name and avatar) → /visit #{edit_user_path(user)}
|
* Edit my profile (including your name and avatar) → /visit #{edit_user_path(user)}
|
||||||
* Manage users → /visit #{account_settings_path}
|
* Manage users → /visit #{account_settings_path}
|
||||||
* Account settings → /visit #{account_settings_path}
|
* Account settings → /visit #{account_settings_path}
|
||||||
|
|
||||||
---------------------------- CRUCIAL DON’TS ---------------------------
|
---------------------------- CRUCIAL DON’TS ---------------------------
|
||||||
|
|
||||||
* Never use names, tags, or stage names mentioned **inside commands** (like /assign, /tag, /stage) as filters.
|
* Never use names, tags, or stage names mentioned **inside commands** (like /assign, /tag, /stage) as filters.
|
||||||
– e.g., “assign to jason” → only /assign jason (NOT assignee_ids)
|
– e.g., “assign to jason” → only /assign jason (NOT assignee_ids)
|
||||||
– e.g., “set the stage to Investigating” → only /stage Investigating (NOT terms)
|
– e.g., “set the stage to Investigating” → only /stage Investigating (NOT terms)
|
||||||
* Never duplicate the assignee in both `commands` and `context`.
|
* Never duplicate the assignee in both commands and context.
|
||||||
– If the request says “assign to X”, produce only `/assign X`, never assignee_ids
|
– If the request says “assign to X”, produce only /assign X, never assignee_ids
|
||||||
* Never add properties tied to UI view ("card", "list", etc.).
|
* Never add properties tied to UI view ("card", "list", etc.).
|
||||||
* 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').
|
* To filter completed or closed cards, use "indexed_by: closed", don't set a "completion" 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.
|
* All filters, including terms, must live **inside** context.
|
||||||
* Do not duplicate terms across properties.
|
* Do not duplicate terms across properties.
|
||||||
* Avoid redundant terms.
|
* Avoid redundant terms.
|
||||||
|
|
||||||
---------------------------- OUTPUT CLEANLINESS ----------------------------
|
---------------------------- OUTPUT CLEANLINESS ----------------------------
|
||||||
|
|
||||||
* Only include context keys that have a meaningful, non-empty value.
|
* Only include context keys that have a meaningful, non-empty value.
|
||||||
– Do NOT include empty arrays (e.g., [], []).
|
– Do NOT include empty arrays (e.g., [], []).
|
||||||
– Do NOT include empty strings ("") or default values that don't apply.
|
– Do NOT include empty strings ("") or default values that don't apply.
|
||||||
– Do NOT emit unused or null context keys — omit them entirely.
|
– Do NOT emit unused or null context keys — omit them entirely.
|
||||||
– Example of bad output: {context: {terms: ["123"], card_ids: [], creator_id: ""}}
|
– Example of bad output: {context: {terms: ["123"], card_ids: [], creator_id: ""}}
|
||||||
✅ Instead: {context: {terms: ["123"]}}
|
✅ Instead: {context: {terms: ["123"]}}
|
||||||
|
|
||||||
* Similarly, only include commands if there are valid actions.
|
* Similarly, only include commands if there are valid actions.
|
||||||
|
|
||||||
---------------------- POSITIVE & NEGATIVE EXAMPLES -------------------
|
---------------------- POSITIVE & NEGATIVE EXAMPLES -------------------
|
||||||
|
|
||||||
User: assign andy to the current #design cards assigned to jz and tag them with #v2
|
User: assign andy to the current #design cards assigned to jz and tag them with #v2
|
||||||
Output:
|
Output:
|
||||||
{
|
{
|
||||||
"context": { "assignee_ids": ["jz"], "tag_ids": ["design"] },
|
"context": { "assignee_ids": ["jz"], "tag_ids": ["design"] },
|
||||||
"commands": ["/assign andy", "/tag #v2"]
|
"commands": ["/assign andy", "/tag #v2"]
|
||||||
}
|
}
|
||||||
|
|
||||||
User: assign to jz
|
User: assign to jz
|
||||||
Output:
|
Output:
|
||||||
{
|
{
|
||||||
"commands": ["/assign jz"]
|
"commands": ["/assign jz"]
|
||||||
}
|
}
|
||||||
|
|
||||||
User: cards assigned to jz
|
User: cards assigned to jz
|
||||||
Output:
|
Output:
|
||||||
{
|
{
|
||||||
"context": { "assignee_ids": ["jz"] }
|
"context": { "assignee_ids": ["jz"] }
|
||||||
}
|
}
|
||||||
|
|
||||||
User: tag with #design
|
User: tag with #design
|
||||||
Output:
|
Output:
|
||||||
{
|
{
|
||||||
"commands": ["/tag #design"]
|
"commands": ["/tag #design"]
|
||||||
}
|
}
|
||||||
|
|
||||||
User: "cards tagged with #design" or "#design cards"
|
User: completed cards
|
||||||
|
Output:
|
||||||
|
{
|
||||||
|
"context": { "indexed_by": "closed" }
|
||||||
|
}
|
||||||
|
|
||||||
|
User: completed cards yesterday
|
||||||
|
Output:
|
||||||
|
{
|
||||||
|
"context": { "indexed_by": "closed", "completion": "yesterday" }
|
||||||
|
}
|
||||||
|
|
||||||
|
User: "cards tagged with #design" or "#design cards"
|
||||||
Output:
|
Output:
|
||||||
{
|
{
|
||||||
"context": { "tag_ids": ["design"] }
|
"context": { "tag_ids": ["design"] }
|
||||||
}
|
}
|
||||||
|
|
||||||
User: Unassigned cards
|
User: Unassigned cards
|
||||||
Output:
|
Output:
|
||||||
{
|
{
|
||||||
"context": { "assignment_status": "unassigned" }
|
"context": { "assignment_status": "unassigned" }
|
||||||
}
|
}
|
||||||
|
|
||||||
User: Close Andy’s cards, then assign them to Kevin
|
User: Close Andy’s cards, then assign them to Kevin
|
||||||
Output:
|
Output:
|
||||||
{
|
{
|
||||||
"context": { "assignee_ids": ["andy"] },
|
"context": { "assignee_ids": ["andy"] },
|
||||||
"commands": ["/close", "/assign kevin"]
|
"commands": ["/close", "/assign kevin"]
|
||||||
}
|
}
|
||||||
|
|
||||||
Fallback search example (when nothing matches):
|
User: cards created yesterday
|
||||||
|
Output:
|
||||||
|
{
|
||||||
|
"context": { "creation": "yesterday" }
|
||||||
|
}
|
||||||
|
|
||||||
|
User: cards completed last week
|
||||||
|
Output:
|
||||||
|
{
|
||||||
|
"context": { "completion": "lastweek", "indexed_by": "closed" }
|
||||||
|
}
|
||||||
|
|
||||||
|
Fallback search example (when nothing matches):
|
||||||
{ "commands": ["/search what's blocking deploy"] }
|
{ "commands": ["/search what's blocking deploy"] }
|
||||||
|
|
||||||
---------------------------- END OF PROMPT ---------------------------
|
---------------------------- END OF PROMPT ---------------------------
|
||||||
PROMPT
|
PROMPT
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class Filter < ApplicationRecord
|
|||||||
@cards ||= begin
|
@cards ||= begin
|
||||||
result = creator.accessible_cards.indexed_by(indexed_by)
|
result = creator.accessible_cards.indexed_by(indexed_by)
|
||||||
result = result.where(id: card_ids) if card_ids.present?
|
result = result.where(id: card_ids) if card_ids.present?
|
||||||
result = result.open unless indexed_by.closed? || card_ids.present?
|
result = result.open unless indexed_by.closed? || close_window || card_ids.present?
|
||||||
result = result.by_engagement_status(engagement_status) if engagement_status.present?
|
result = result.by_engagement_status(engagement_status) if engagement_status.present?
|
||||||
result = result.unassigned if assignment_status.unassigned?
|
result = result.unassigned if assignment_status.unassigned?
|
||||||
result = result.assigned_to(assignees.ids) if assignees.present?
|
result = result.assigned_to(assignees.ids) if assignees.present?
|
||||||
@@ -28,6 +28,8 @@ class Filter < ApplicationRecord
|
|||||||
result = result.where(collection: collections.ids) if collections.present?
|
result = result.where(collection: collections.ids) if collections.present?
|
||||||
result = result.in_stage(stages.ids) if stages.present?
|
result = result.in_stage(stages.ids) if stages.present?
|
||||||
result = result.tagged_with(tags.ids) if tags.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 = terms.reduce(result) do |result, term|
|
result = terms.reduce(result) do |result, term|
|
||||||
result.mentioning(term)
|
result.mentioning(term)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ module Filter::Fields
|
|||||||
end
|
end
|
||||||
|
|
||||||
included do
|
included do
|
||||||
store_accessor :fields, :assignment_status, :indexed_by, :terms, :engagement_status, :card_ids
|
store_accessor :fields, :assignment_status, :indexed_by, :terms,
|
||||||
|
:engagement_status, :card_ids, :creation, :close
|
||||||
|
|
||||||
def assignment_status
|
def assignment_status
|
||||||
super.to_s.inquiry
|
super.to_s.inquiry
|
||||||
@@ -30,6 +31,14 @@ module Filter::Fields
|
|||||||
super&.inquiry
|
super&.inquiry
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def creation_window
|
||||||
|
TimeWindowParser.parse(creation)
|
||||||
|
end
|
||||||
|
|
||||||
|
def close_window
|
||||||
|
TimeWindowParser.parse(close)
|
||||||
|
end
|
||||||
|
|
||||||
def terms
|
def terms
|
||||||
Array(super)
|
Array(super)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ module Filter::Params
|
|||||||
:assignment_status,
|
:assignment_status,
|
||||||
:indexed_by,
|
:indexed_by,
|
||||||
:engagement_status,
|
:engagement_status,
|
||||||
|
:creation,
|
||||||
|
:close,
|
||||||
card_ids: [],
|
card_ids: [],
|
||||||
assignee_ids: [],
|
assignee_ids: [],
|
||||||
creator_ids: [],
|
creator_ids: [],
|
||||||
@@ -38,6 +40,8 @@ module Filter::Params
|
|||||||
{}.tap do |params|
|
{}.tap do |params|
|
||||||
params[:indexed_by] = indexed_by
|
params[:indexed_by] = indexed_by
|
||||||
params[:engagement_status] = engagement_status
|
params[:engagement_status] = engagement_status
|
||||||
|
params[:creation] = creation
|
||||||
|
params[:close] = close
|
||||||
params[:assignment_status] = assignment_status
|
params[:assignment_status] = assignment_status
|
||||||
params[:terms] = terms
|
params[:terms] = terms
|
||||||
params[:tag_ids] = tags.ids
|
params[:tag_ids] = tags.ids
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
class TimeWindowParser
|
||||||
|
attr_reader :now
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def parse(string)
|
||||||
|
new.parse(string)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(now: Time.current)
|
||||||
|
@now = now
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse(string)
|
||||||
|
case normalize(string)
|
||||||
|
when "today"
|
||||||
|
now.beginning_of_day..now.end_of_day
|
||||||
|
when "yesterday"
|
||||||
|
(now - 1.day).beginning_of_day..(now - 1.day).end_of_day
|
||||||
|
when "thisweek"
|
||||||
|
now.beginning_of_week..now.end_of_week
|
||||||
|
when "thismonth"
|
||||||
|
now.beginning_of_month..now.end_of_month
|
||||||
|
when "thisyear"
|
||||||
|
now.beginning_of_year..now.end_of_year
|
||||||
|
when "lastweek"
|
||||||
|
(now - 1.week).beginning_of_week..(now - 1.week).end_of_week
|
||||||
|
when "lastmonth"
|
||||||
|
(now - 1.month).beginning_of_month..(now - 1.month).end_of_month
|
||||||
|
when "lastyear"
|
||||||
|
(now - 1.year).beginning_of_year..(now - 1.year).end_of_year
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def normalize(string)
|
||||||
|
if string
|
||||||
|
string.downcase.gsub(/[\s_\-]/, "")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -121,6 +121,13 @@ class Command::Ai::TranslatorTest < ActionDispatch::IntegrationTest
|
|||||||
assert_command({ commands: [ "/add_card urgent issue" ] }, "create card urgent issue")
|
assert_command({ commands: [ "/add_card urgent issue" ] }, "create card urgent issue")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
vcr_record!
|
||||||
|
|
||||||
|
test "filter by time ranges" do
|
||||||
|
assert_command({context: {completion: "thisweek", indexed_by: "closed"}}, "cards completed this week")
|
||||||
|
assert_command({context: {creation: "thisweek", tag_ids: ["design"], creator_id: "jz"}}, "cards created this week by jz tagged as #design")
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def assert_command(expected, query, context: :list)
|
def assert_command(expected, query, context: :list)
|
||||||
assert_equal expected, translate(query, context:)
|
assert_equal expected, translate(query, context:)
|
||||||
|
|||||||
@@ -133,4 +133,24 @@ class FilterTest < ActiveSupport::TestCase
|
|||||||
assert filter.indexed_by.closed?
|
assert filter.indexed_by.closed?
|
||||||
assert_equal [ "haggis" ], filter.terms
|
assert_equal [ "haggis" ], filter.terms
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "creation window" do
|
||||||
|
filter = users(:david).filters.new creation: "this week"
|
||||||
|
|
||||||
|
cards(:logo).update_columns created_at: 2.weeks.ago
|
||||||
|
assert_not_includes filter.cards, cards(:logo)
|
||||||
|
|
||||||
|
cards(:logo).update_columns created_at: Time.current
|
||||||
|
assert_includes filter.cards, cards(:logo)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "close window" do
|
||||||
|
filter = users(:david).filters.new close: "this week"
|
||||||
|
|
||||||
|
cards(:shipping).closure.update_columns created_at: 2.weeks.ago
|
||||||
|
assert_not_includes filter.cards, cards(:shipping)
|
||||||
|
|
||||||
|
cards(:shipping).closure.update_columns created_at: Time.current
|
||||||
|
assert_includes filter.cards, cards(:shipping)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class TimeWindowParserTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
@now = Time.zone.parse("2023-06-15 9am")
|
||||||
|
@parser = TimeWindowParser.new(now: @now)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parse today" do
|
||||||
|
assert_equal @now.beginning_of_day..@now.end_of_day,
|
||||||
|
@parser.parse("today")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parse yesterday" do
|
||||||
|
yesterday = @now - 1.day
|
||||||
|
|
||||||
|
assert_equal yesterday.beginning_of_day..yesterday.end_of_day,
|
||||||
|
@parser.parse("yesterday")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parse this week" do
|
||||||
|
assert_equal @now.beginning_of_week..@now.end_of_week,
|
||||||
|
@parser.parse("this week")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parse this month" do
|
||||||
|
assert_equal @now.beginning_of_month..@now.end_of_month,
|
||||||
|
@parser.parse("this month")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parse this year" do
|
||||||
|
assert_equal @now.beginning_of_year..@now.end_of_year,
|
||||||
|
@parser.parse("this year")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parse last week" do
|
||||||
|
last_week = @now - 1.week
|
||||||
|
|
||||||
|
assert_equal last_week.beginning_of_week..last_week.end_of_week,
|
||||||
|
@parser.parse("last week")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parse last month" do
|
||||||
|
last_month = @now - 1.month
|
||||||
|
|
||||||
|
assert_equal last_month.beginning_of_month..last_month.end_of_month,
|
||||||
|
@parser.parse("last month")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parse last year" do
|
||||||
|
last_year = @now - 1.year
|
||||||
|
|
||||||
|
assert_equal last_year.beginning_of_year..last_year.end_of_year,
|
||||||
|
@parser.parse("last year")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parse with unknown string returns nil" do
|
||||||
|
assert_nil @parser.parse("unknown time window")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns nil for nil" do
|
||||||
|
assert_nil @parser.parse(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
+1408
File diff suppressed because it is too large
Load Diff
+1420
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
+5615
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
+1404
File diff suppressed because it is too large
Load Diff
+1407
File diff suppressed because it is too large
Load Diff
+1408
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user