support_desk 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +72 -0
- data/README.md +113 -12
- data/Rakefile +16 -0
- data/app/controllers/support_desk/tickets_controller.rb +11 -3
- data/app/helpers/support_desk/engine_helper.rb +7 -1
- data/app/views/support_desk/console/tickets/_context_card.html.erb +5 -0
- data/app/views/support_desk/console/tickets/_new_conversation_form.html.erb +91 -0
- data/app/views/support_desk/console/tickets/_ticket_row.html.erb +8 -0
- data/app/views/support_desk/console/tickets/index.html.erb +14 -2
- data/app/views/support_desk/console/tickets/new.html.erb +25 -0
- data/app/views/support_desk/tickets/_write.html.erb +1 -0
- data/config/console_routes.rb +1 -1
- data/config/locales/support_desk.console.en.yml +26 -0
- data/config/locales/support_desk.console.es.yml +26 -0
- data/config/locales/support_desk.en.yml +3 -0
- data/config/locales/support_desk.es.yml +6 -0
- data/lib/generators/support_desk/console_generator.rb +1 -1
- data/lib/generators/support_desk/install_generator.rb +14 -0
- data/lib/generators/support_desk/templates/add_opened_by_to_support_desk_tickets.rb.erb +79 -0
- data/lib/generators/support_desk/templates/console/controller.rb.erb +1 -1
- data/lib/generators/support_desk/templates/initializer.rb +30 -0
- data/lib/generators/support_desk/upgrade_generator.rb +56 -0
- data/lib/support_desk/configuration.rb +148 -2
- data/lib/support_desk/console.rb +358 -26
- data/lib/support_desk/console_engine.rb +2 -0
- data/lib/support_desk/console_routes.rb +20 -8
- data/lib/support_desk/context_card.rb +23 -0
- data/lib/support_desk/doctor.rb +51 -1
- data/lib/support_desk/errors.rb +5 -0
- data/lib/support_desk/macros.rb +28 -12
- data/lib/support_desk/models/assignment.rb +3 -1
- data/lib/support_desk/models/concerns/agent.rb +39 -4
- data/lib/support_desk/models/concerns/requester.rb +27 -12
- data/lib/support_desk/models/ticket.rb +509 -126
- data/lib/support_desk/summary.rb +1 -1
- data/lib/support_desk/test_helpers.rb +8 -3
- data/lib/support_desk/topic.rb +8 -29
- data/lib/support_desk/version.rb +1 -1
- data/lib/support_desk/wizard.rb +10 -27
- data/lib/support_desk.rb +45 -0
- data/lib/tasks/support_desk.rake +27 -0
- metadata +13 -2
data/lib/support_desk/summary.rb
CHANGED
|
@@ -46,7 +46,7 @@ module SupportDesk
|
|
|
46
46
|
# this speaks the reader's language. Duration#inspect is English
|
|
47
47
|
# whatever the locale, which left one untranslatable string in an
|
|
48
48
|
# otherwise Spanish console.
|
|
49
|
-
|
|
49
|
+
SupportDesk.humanize_duration(duration)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
# The whole line.
|
|
@@ -6,6 +6,7 @@ module SupportDesk
|
|
|
6
6
|
# include SupportDesk::TestHelpers
|
|
7
7
|
#
|
|
8
8
|
# ticket = open_support_ticket(for: users(:alice), about: rides(:sevilla), message: "No aparece")
|
|
9
|
+
# ticket = open_support_ticket(for: users(:alice), by: users(:lucia), message: "Vimos que…")
|
|
9
10
|
# reply_as users(:lucia), ticket, "Lo miramos"
|
|
10
11
|
# assert_awaiting_requester ticket
|
|
11
12
|
# assert_ticket_event ticket, :handed_off, from: users(:lucia), to: users(:pedro)
|
|
@@ -15,10 +16,14 @@ module SupportDesk
|
|
|
15
16
|
# The assertions read the same way the gem's own suite does, which is the
|
|
16
17
|
# point: your acceptance tests and ours describe the same behaviour.
|
|
17
18
|
module TestHelpers
|
|
18
|
-
# Open a ticket the way a requester would,
|
|
19
|
-
|
|
19
|
+
# Open a ticket the way a requester would — or, with `by:`, the way the
|
|
20
|
+
# desk does when it writes first. Through the public sugar in both cases,
|
|
21
|
+
# so this helper exercises what hosts actually type.
|
|
22
|
+
def open_support_ticket(message: "Necesito ayuda", about: nil, topic: nil, by: nil, **options)
|
|
20
23
|
requester = options.fetch(:for) { raise ArgumentError, "open_support_ticket needs for: a requester" }
|
|
21
|
-
requester.ask_support!(message, about: about, topic: topic)
|
|
24
|
+
return requester.ask_support!(message, about: about, topic: topic) if by.nil? || by == requester
|
|
25
|
+
|
|
26
|
+
by.open_support_conversation_with!(requester, message, about: about, topic: topic)
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
# Answer as an agent. Returns the Chats::Message.
|
data/lib/support_desk/topic.rb
CHANGED
|
@@ -192,9 +192,12 @@ module SupportDesk
|
|
|
192
192
|
|
|
193
193
|
# The desk key tickets under this topic belong to.
|
|
194
194
|
def desk_key
|
|
195
|
-
|
|
195
|
+
desk_override || :default
|
|
196
196
|
end
|
|
197
197
|
|
|
198
|
+
# nil means inherit the wizard's desk, not route to the default desk.
|
|
199
|
+
def desk_override = inherited_or_own(:desk)&.to_sym
|
|
200
|
+
|
|
198
201
|
# Hidden from the wizard; the label still resolves for historic tickets.
|
|
199
202
|
def retired? = !!inherited_or_own(:retired)
|
|
200
203
|
|
|
@@ -257,34 +260,10 @@ module SupportDesk
|
|
|
257
260
|
def inspect = "#<SupportDesk::Topic::Unknown #{path}>"
|
|
258
261
|
end
|
|
259
262
|
|
|
260
|
-
#
|
|
261
|
-
#
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
class Type < ActiveModel::Type::Value
|
|
265
|
-
def type = :string
|
|
266
|
-
|
|
267
|
-
def cast(value)
|
|
268
|
-
case value
|
|
269
|
-
when nil then nil
|
|
270
|
-
when Topic then value
|
|
271
|
-
else SupportDesk.find_topic(value.to_s)
|
|
272
|
-
end
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def serialize(value)
|
|
276
|
-
case value
|
|
277
|
-
when nil then nil
|
|
278
|
-
when Topic then value.path
|
|
279
|
-
else value.to_s
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
def deserialize(value) = cast(value)
|
|
284
|
-
|
|
285
|
-
def changed_in_place?(raw_old_value, new_value)
|
|
286
|
-
raw_old_value != serialize(new_value)
|
|
287
|
-
end
|
|
263
|
+
# Casting must not consult a global topic tree: the same path may mean
|
|
264
|
+
# different things on two desks. Ticket#topic resolves the stored path.
|
|
265
|
+
class Type < ActiveModel::Type::String
|
|
266
|
+
def cast(value) = value&.to_s
|
|
288
267
|
end
|
|
289
268
|
end
|
|
290
269
|
end
|
data/lib/support_desk/version.rb
CHANGED
data/lib/support_desk/wizard.rb
CHANGED
|
@@ -84,7 +84,7 @@ module SupportDesk
|
|
|
84
84
|
# requester's desk, and a #desk that depended on the topic would ask the
|
|
85
85
|
# tree for the topic to find out which tree to ask.
|
|
86
86
|
def target_desk
|
|
87
|
-
key = topic&.
|
|
87
|
+
key = topic&.desk_override
|
|
88
88
|
return desk if key.nil? || key == desk.key
|
|
89
89
|
|
|
90
90
|
SupportDesk.desk(key) || desk
|
|
@@ -186,20 +186,12 @@ module SupportDesk
|
|
|
186
186
|
end
|
|
187
187
|
|
|
188
188
|
# The promise as a Duration, for hosts that want to phrase it themselves.
|
|
189
|
-
def promise_within =
|
|
189
|
+
def promise_within = target_desk.config.reply_within
|
|
190
190
|
|
|
191
|
-
# "1 day", "4 horas" —
|
|
192
|
-
#
|
|
193
|
-
#
|
|
194
|
-
def self.humanize_duration(duration)
|
|
195
|
-
return duration.inspect unless defined?(ActionView::Helpers::DateHelper)
|
|
196
|
-
|
|
197
|
-
@duration_words ||= Object.new.extend(ActionView::Helpers::DateHelper)
|
|
198
|
-
words = @duration_words.distance_of_time_in_words(duration.to_i).to_s
|
|
199
|
-
# An app whose locale has no date translations (no rails-i18n) would
|
|
200
|
-
# otherwise show "Translation missing" to a customer.
|
|
201
|
-
words.start_with?("Translation missing") ? duration.inspect : words
|
|
202
|
-
end
|
|
191
|
+
# "1 day", "4 horas" — the summary, the helper and an opening line's
|
|
192
|
+
# `%{reply_within}` all say it too, so it lives on SupportDesk now. Kept
|
|
193
|
+
# here as a delegation: it has been public since 0.1.
|
|
194
|
+
def self.humanize_duration(duration) = SupportDesk.humanize_duration(duration)
|
|
203
195
|
|
|
204
196
|
# The open case this would land in, when the requester already has one
|
|
205
197
|
# about this thing — so the wizard can say "you already have a
|
|
@@ -207,10 +199,7 @@ module SupportDesk
|
|
|
207
199
|
def existing_ticket
|
|
208
200
|
return nil if topic.nil? || topic.branch?
|
|
209
201
|
|
|
210
|
-
|
|
211
|
-
return nil if key.start_with?("free:")
|
|
212
|
-
|
|
213
|
-
Ticket.not_closed.find_by(requester: requester, desk: target_desk, cardinality_key: key)
|
|
202
|
+
Ticket.existing_for(requester: requester, desk: target_desk, subject: subject, topic: topic)
|
|
214
203
|
end
|
|
215
204
|
|
|
216
205
|
# Which records in +choices+ the requester already has an open case
|
|
@@ -270,15 +259,9 @@ module SupportDesk
|
|
|
270
259
|
raise InvalidTransition, "the wizard is still on the #{step} step — pick one before submitting"
|
|
271
260
|
end
|
|
272
261
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
requester.ask_support!(message, about: subject, topic: topic.path, files: files)
|
|
277
|
-
else
|
|
278
|
-
Ticket.open!(requester: requester, message: message, about: subject, topic: topic.path,
|
|
279
|
-
files: files, desk: target_desk,
|
|
280
|
-
requester_role: requester.class.support_desk_requester_options[:as])
|
|
281
|
-
end
|
|
262
|
+
Ticket.open!(requester: requester, message: message, about: subject, topic: topic.path,
|
|
263
|
+
files: files, desk: target_desk,
|
|
264
|
+
requester_role: requester.class.support_desk_requester_options[:as])
|
|
282
265
|
end
|
|
283
266
|
|
|
284
267
|
# A signed token for the currently chosen subject, to round-trip through
|
data/lib/support_desk.rb
CHANGED
|
@@ -158,6 +158,17 @@ module SupportDesk
|
|
|
158
158
|
# Every class that has declared itself able to answer.
|
|
159
159
|
def agent_class_names = @agent_class_names ||= Set.new
|
|
160
160
|
|
|
161
|
+
# The registered classes themselves, for the places that have to restrict
|
|
162
|
+
# a lookup to them — the console's GlobalID allow-lists, where a raw
|
|
163
|
+
# token is an identifier and never an authorization to call `find` on
|
|
164
|
+
# whatever class name it names.
|
|
165
|
+
#
|
|
166
|
+
# Names that no longer resolve are dropped rather than raised on: the
|
|
167
|
+
# registries store names so they survive reloads, and a class that went
|
|
168
|
+
# away is simply not one of the classes people can be looked up as.
|
|
169
|
+
def requester_classes = resolve_all(requester_class_names)
|
|
170
|
+
def supportable_classes = resolve_all(supportable_class_names)
|
|
171
|
+
|
|
161
172
|
# Whether +klass+ (a Class, an instance, or a class name) is supportable.
|
|
162
173
|
def supportable_class?(klass) = registered?(supportable_class_names, klass)
|
|
163
174
|
# Whether +klass+ asks for support / answers it. Ancestor-aware, so an
|
|
@@ -165,6 +176,20 @@ module SupportDesk
|
|
|
165
176
|
def requester_class?(klass) = registered?(requester_class_names, klass)
|
|
166
177
|
def agent_class?(klass) = registered?(agent_class_names, klass)
|
|
167
178
|
|
|
179
|
+
# --- Macro conditions -----------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
# Whether +record+ passes an `if:` condition from a macro — nil is always
|
|
182
|
+
# yes. Both `has_support_tickets if:` (may this person ask for help, and
|
|
183
|
+
# be written to) and `acts_as_support_agent if:` (may this person answer)
|
|
184
|
+
# are read through here, so the two conditions can never drift into two
|
|
185
|
+
# meanings of the same option.
|
|
186
|
+
def eligible?(record, condition)
|
|
187
|
+
return true if condition.nil?
|
|
188
|
+
return !!record.public_send(condition) if condition.is_a?(Symbol)
|
|
189
|
+
|
|
190
|
+
!!condition.call(record)
|
|
191
|
+
end
|
|
192
|
+
|
|
168
193
|
# --- The chats seam -------------------------------------------------------------
|
|
169
194
|
|
|
170
195
|
# Subscribe the gem's own `:message_created` listener, which is what
|
|
@@ -257,6 +282,22 @@ module SupportDesk
|
|
|
257
282
|
]
|
|
258
283
|
end
|
|
259
284
|
|
|
285
|
+
# A duration in the reader's own language ("1 día", "4 hours") — the
|
|
286
|
+
# answer promise in the wizard's line, the wait in a console summary, and
|
|
287
|
+
# the `%{reply_within}` an opening line can interpolate.
|
|
288
|
+
#
|
|
289
|
+
# Duration#inspect is English whatever the locale, which is what left one
|
|
290
|
+
# untranslatable string in an otherwise Spanish console.
|
|
291
|
+
def humanize_duration(duration)
|
|
292
|
+
return duration.inspect unless defined?(ActionView::Helpers::DateHelper)
|
|
293
|
+
|
|
294
|
+
@duration_words ||= Object.new.extend(ActionView::Helpers::DateHelper)
|
|
295
|
+
words = @duration_words.distance_of_time_in_words(duration.to_i).to_s
|
|
296
|
+
# An app whose locale has no date translations (no rails-i18n) would
|
|
297
|
+
# otherwise show "Translation missing" to a customer.
|
|
298
|
+
words.start_with?("Translation missing") ? duration.inspect : words
|
|
299
|
+
end
|
|
300
|
+
|
|
260
301
|
# A stable, URL-safe key for an actor (agent, requester, desk), used in
|
|
261
302
|
# cache keys and event payloads. GlobalID params already encode class +
|
|
262
303
|
# id, so two classes can never collide.
|
|
@@ -274,6 +315,10 @@ module SupportDesk
|
|
|
274
315
|
klass
|
|
275
316
|
end
|
|
276
317
|
|
|
318
|
+
def resolve_all(registry)
|
|
319
|
+
registry.filter_map { |name| name.safe_constantize }
|
|
320
|
+
end
|
|
321
|
+
|
|
277
322
|
def registered?(registry, klass)
|
|
278
323
|
klass = klass.class unless klass.is_a?(Class) || klass.is_a?(String)
|
|
279
324
|
name = klass.is_a?(String) ? klass : klass.name
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :support_desk do
|
|
4
|
+
desc "Point every case with no provenance at its requester (the 0.1 → 0.2 catch-up)"
|
|
5
|
+
task backfill_opened_by: :environment do
|
|
6
|
+
# The same UPDATE the 0.2.0 migration runs, for the handover window: a
|
|
7
|
+
# 0.1 process still serving traffic writes NULL provenance after the
|
|
8
|
+
# migration's own backfill, so the catch-up is run once more when those
|
|
9
|
+
# processes are gone.
|
|
10
|
+
#
|
|
11
|
+
# Idempotent, and safe to run at any time, for one reason: 0.2 opens
|
|
12
|
+
# every case as a record (automation openers are refused — see
|
|
13
|
+
# docs/12-open-questions.md Q17), so a NULL `opened_by` can only be a row
|
|
14
|
+
# 0.1 wrote. The day automation lands, NULL becomes legitimate provenance
|
|
15
|
+
# and this task stops being safe — that is part of the work Q17 names.
|
|
16
|
+
relation = SupportDesk::Ticket.where(opened_by_id: nil)
|
|
17
|
+
pending = relation.count
|
|
18
|
+
|
|
19
|
+
if pending.zero?
|
|
20
|
+
puts "[support_desk] every case already says who opened it."
|
|
21
|
+
next
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
updated = relation.update_all("opened_by_type = requester_type, opened_by_id = requester_id")
|
|
25
|
+
puts "[support_desk] #{updated} case(s) now point at their requester."
|
|
26
|
+
end
|
|
27
|
+
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: support_desk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-09-
|
|
10
|
+
date: 2026-09-18 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activerecord
|
|
@@ -56,6 +56,9 @@ dependencies:
|
|
|
56
56
|
- - "~>"
|
|
57
57
|
- !ruby/object:Gem::Version
|
|
58
58
|
version: '0.3'
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: 0.3.2
|
|
59
62
|
type: :runtime
|
|
60
63
|
prerelease: false
|
|
61
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -63,6 +66,9 @@ dependencies:
|
|
|
63
66
|
- - "~>"
|
|
64
67
|
- !ruby/object:Gem::Version
|
|
65
68
|
version: '0.3'
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: 0.3.2
|
|
66
72
|
- !ruby/object:Gem::Dependency
|
|
67
73
|
name: globalid
|
|
68
74
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -138,11 +144,13 @@ files:
|
|
|
138
144
|
- app/views/support_desk/console/tickets/_context_card.html.erb
|
|
139
145
|
- app/views/support_desk/console/tickets/_message.html.erb
|
|
140
146
|
- app/views/support_desk/console/tickets/_nav_badge.html.erb
|
|
147
|
+
- app/views/support_desk/console/tickets/_new_conversation_form.html.erb
|
|
141
148
|
- app/views/support_desk/console/tickets/_tabs.html.erb
|
|
142
149
|
- app/views/support_desk/console/tickets/_ticket_row.html.erb
|
|
143
150
|
- app/views/support_desk/console/tickets/_timeline.html.erb
|
|
144
151
|
- app/views/support_desk/console/tickets/_transcript.html.erb
|
|
145
152
|
- app/views/support_desk/console/tickets/index.html.erb
|
|
153
|
+
- app/views/support_desk/console/tickets/new.html.erb
|
|
146
154
|
- app/views/support_desk/console/tickets/show.html.erb
|
|
147
155
|
- app/views/support_desk/tickets/_context_card.html.erb
|
|
148
156
|
- app/views/support_desk/tickets/_door.html.erb
|
|
@@ -166,10 +174,12 @@ files:
|
|
|
166
174
|
- gemfiles/rails_8.1.gemfile
|
|
167
175
|
- lib/generators/support_desk/console_generator.rb
|
|
168
176
|
- lib/generators/support_desk/install_generator.rb
|
|
177
|
+
- lib/generators/support_desk/templates/add_opened_by_to_support_desk_tickets.rb.erb
|
|
169
178
|
- lib/generators/support_desk/templates/console/controller.rb.erb
|
|
170
179
|
- lib/generators/support_desk/templates/console/resource.rb.erb
|
|
171
180
|
- lib/generators/support_desk/templates/create_support_desk_tables.rb.erb
|
|
172
181
|
- lib/generators/support_desk/templates/initializer.rb
|
|
182
|
+
- lib/generators/support_desk/upgrade_generator.rb
|
|
173
183
|
- lib/generators/support_desk/views_generator.rb
|
|
174
184
|
- lib/support_desk.rb
|
|
175
185
|
- lib/support_desk/configuration.rb
|
|
@@ -199,6 +209,7 @@ files:
|
|
|
199
209
|
- lib/support_desk/topic_tree.rb
|
|
200
210
|
- lib/support_desk/version.rb
|
|
201
211
|
- lib/support_desk/wizard.rb
|
|
212
|
+
- lib/tasks/support_desk.rake
|
|
202
213
|
homepage: https://github.com/rameerez/support_desk
|
|
203
214
|
licenses:
|
|
204
215
|
- MIT
|