feat(zendesk): smart actions for ticket creation and closure#303
Open
christophebrun-forest wants to merge 9 commits into
Open
feat(zendesk): smart actions for ticket creation and closure#303christophebrun-forest wants to merge 9 commits into
christophebrun-forest wants to merge 9 commits into
Conversation
… actions
Two smart actions on the Zendesk datasource, both configurable from
Datasource.new and reusable on any host collection via register_on:
- CreateTicketWithNotification (auto-registered on ZendeskUser)
Opens a ticket from a host record. The requester is identified by an
email entered in the form, optionally pre-filled by requester_email_default
(literal String at datasource level, String or Proc on register_on).
Subject and Message defaults support {{record.<field>}} interpolation.
Message uses the RichText widget and ships to Zendesk as html_body.
Optional ticket_id_field on register_on writes the new ticket id back to
a configured field on the host record (best-effort: failure logs a warn
and surfaces in the success message without rolling back the ticket).
- CloseTicket (opt-in on ZendeskTicket)
Two no-form actions per status (Single + Bulk) that flip the ticket status
to solved or closed. Opt-in via close_ticket_statuses: %w[solved closed]
on Datasource.new (empty by default).
Internal: BaseCollection delegates execute/get_form to an internal
ActionCollectionDecorator so actions registered at the datasource level get
the full form lifecycle (defaults, conditions, watch_changes) that the agent
applies to customizer-defined actions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-up fixes from code review:
- CreateTicketWithNotification: HTML-escape {{record.<field>}} tokens when
interpolating into the Message template. Without this, a record value
containing `<`, `&`, or markup would break the outbound HTML or smuggle
markup into the email Zendesk triggers send to the requester. Subject
interpolation remains unescaped since it's a plain-text field.
- CloseTicket: walk ticket ids one by one inside the executor instead of
letting the first Zendesk API error abort the rest of a bulk run. The
most common case is Zendesk rejecting the direct open -> closed
transition; previously the entire action returned a 500. Now:
* All ids succeed: Success with the count.
* Some fail (bulk): Success with the success count + list of failed ids.
* All fail: Error with the underlying API reason.
Each failure is also logged via the package logger.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up review nits, none functional: - CreateTicketWithNotification: log a warn before swallowing StandardError in `requester_default` and `fetch_record`. Previously, a typo in a user-supplied resolver lambda or a transient list failure would leave the email field silently empty with no signal anywhere. - Datasource: `.uniq` on `close_ticket_statuses` so an accidental `%w[solved solved closed]` no longer crashes registration with "Action ... already defined". - ASCII em-dash in the writeback-failure success message replaced by a colon, avoiding encoding hazards on downstream consumers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9 new issues
|
The new files were sitting at 19-35% comment density while the rest of the Zendesk package is at 0-2%. Dropped tutorial-style docstrings and restating comments; kept only the genuinely non-obvious notes (Zendesk open->closed transition, html_body escaping, writeback best-effort, internal ActionCollectionDecorator reuse). Final density: 3-6%, in line with the surrounding code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rrides
Four new optional kwargs on Actions::CreateTicketWithNotification, all
also propagated through Datasource.new for the ZendeskUser auto-registration:
- action_name: overrides the label the action is registered under (defaults
to 'Create ticket and notify' as before).
- email_templates: array of { title:, content: } hashes. When present, the
form becomes a two-page wizard: page 1 picks a template (or 'No template'),
page 2 is the body form with Message pre-filled from the selection.
'No template' yields a strictly empty Message; default_ticket_message is
ignored when templates are configured (strict opt-in to the wizard).
- priority_override: when set, the Priority dropdown is removed from the form
and this value is forced in the payload sent to Zendesk.
- type_override: same for Type. Useful when Zendesk's setup requires those
fields but you don't want the agent to choose.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related bugs in the template wizard:
1. The Message field used `default_value:` so drop_default skipped the
proc once data['Message'] was cached, meaning selecting a template
never refreshed the field. Switch to `value:` (re-evaluated every
form fetch by drop_deferred) and gate the proc on
`field_changed?('Template')`: emit the content when Template just
changed, return nil otherwise so the agent's set_watch_changes
carries over whatever the user typed.
2. `{{record.<field>}}` tokens inside a template's content were not
interpolated. Run the selected template content through the same
HTML-escaping interpolate helper used for default_ticket_message
(short-circuit when no token is present to skip the record fetch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- sender_email kwarg on Datasource.new and register_on. Mapped to
Zendesk's `recipient` field in the create-ticket payload (the support
address the ticket is tied to, which is also the From address of the
notification email sent to the requester). Propagated from the
datasource to the ZendeskUser auto-registered action.
- Fix ZendeskAPI::Error::RecordInvalid 'Requester: Name: is too short':
Zendesk auto-creates the requester user from the email when no match,
but its validation requires a non-empty name. Derive name from the
email's local-part ('john.doe@acme.com' -> 'john.doe') so the create
step succeeds. Ignored when the email maps to an existing user.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops auto-registration on ZendeskTicket / ZendeskUser and slims the Datasource constructor to just credentials. CreateTicketWithNotification and CloseTicket are now opted in per host collection via plugins, with the Zendesk ticket id read from a configurable column on the host record (e.g. last_zendesk_ticket_id). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Inline Actions::* into their Plugins:: counterparts; drop the
actions/ directory and the dead ActionCollectionDecorator plumbing
in BaseCollection (the customizer wraps collections itself now).
- Extract FormBuilder and Messages sub-modules to keep each plugin file
focused on registration and orchestration.
- Share Zendesk enum values via a new TicketEnums module so the schema
and form builder reference a single source.
- CloseTicket: replace the single statuses option with orthogonal
statuses + scopes so callers can pick solved vs closed and single
vs bulk independently.
- CloseTicket: swap Zendesks raw "closed prevents ticket update" stack
for a clean message: success ("was already closed") when targeting
closed, Error ("cannot reopen to mark as solved") when targeting solved.
- Trim over-documented comments and route specs through #run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| end | ||
| end | ||
|
|
||
| def build_action(datasource, status, scope, ticket_id_field) |
| else | ||
| result_builder.success(message: Messages.success(succeeded, already_closed, failed, status)) | ||
| end | ||
| end |
| failed << [id, "#{e.class}: #{e.message}"] | ||
| end | ||
| end | ||
| [succeeded, already_closed, failed] |
| module Messages | ||
| module_function | ||
|
|
||
| def success(succeeded, already_closed, failed, status) |
|
|
||
| writeback = write_back_ticket_id(context, opts[:ticket_id_field], ticket_id) | ||
| result_builder.success(message: success_message(ticket_id, values, writeback)) | ||
| end |
| payload['type'] = type if present?(type) | ||
| # Zendesk's `recipient` = the support address replies come FROM. | ||
| payload['recipient'] = opts[:sender_email] if present?(opts[:sender_email]) | ||
| payload |
| "[forest_admin_datasource_zendesk] requester_email_default resolver raised: #{e.class}: #{e.message}" | ||
| ) | ||
| nil | ||
| end |
| return content unless content.match?(TOKEN_RE) | ||
|
|
||
| interpolate(content, fetch_record(context), escape_html: true) | ||
| end |
| next '' if value.nil? | ||
|
|
||
| escape_html ? CGI.escapeHTML(value.to_s) : value.to_s | ||
| end |
Comment on lines
+102
to
+104
| def present?(value) | ||
| !value.nil? && value.to_s != '' | ||
| end |
There was a problem hiding this comment.
🟢 Low plugins/create_ticket_with_notification.rb:102
The present? helper considers whitespace-only strings as present (" ".present? is true), so an email field containing only spaces passes validation at line 32. The code then calls create_ticket with an invalid whitespace-only email, which likely causes Zendesk API errors or creates malformed tickets. Consider stripping whitespace before the empty check, or using a stricter validation that rejects whitespace-only input.
def present?(value)
- !value.nil? && value.to_s != ''
+ !value.nil? && value.to_s.strip != ''
end🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/plugins/create_ticket_with_notification.rb around lines 102-104:
The `present?` helper considers whitespace-only strings as present (`" ".present?` is true), so an email field containing only spaces passes validation at line 32. The code then calls `create_ticket` with an invalid whitespace-only email, which likely causes Zendesk API errors or creates malformed tickets. Consider stripping whitespace before the empty check, or using a stricter validation that rejects whitespace-only input.
Evidence trail:
packages/forest_admin_datasource_zendesk/lib/forest_admin_datasource_zendesk/plugins/create_ticket_with_notification.rb lines 32 and 102 at REVIEWED_COMMIT. Custom `present?` defined at line 102 checks `!value.nil? && value.to_s != ''` which returns true for whitespace-only strings. Line 32 uses this method to validate email before passing it to `create_ticket` at line 35.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ZendeskUser, and reusable on any host collection viaActions::CreateTicketWithNotification.register_on(collection, datasource, ...). Pre-fills a "Requester email" form field (literal String or Proc resolver), supports{{record.<field>}}token interpolation in Subject/Message defaults (HTML-escaped in the RichText Message to prevent injection), and ships the comment to Zendesk ashtml_body. Optionalticket_id_fieldwrites the new ticket id back to the host record (best-effort: a writeback failure is logged and surfaced in the success message without rolling back the ticket).close_ticket_statuses: %w[solved closed]onDatasource.new). Per-id rescue so a Zendesk transition rejection (e.g.open -> closed) doesn't abort the rest of a bulk run; partial-success surfaces the failed ids in the result message.BaseCollectiondelegatesexecute/get_formto an internalActionCollectionDecoratorso actions registered at the datasource level get the full form lifecycle (defaults, conditions, watch_changes) the agent applies to customizer-defined actions.Test plan
bundle exec rspec-- 220 examples, 0 failures, 99% coveragebundle exec rubocop-- 0 offensesZendeskUserdetail page, verify the requester email is pre-filledregister_on(...)and confirm token interpolation reads the host recordclose_ticket_statuses: %w[solved closed], run a bulk close on a mix of tickets including one in a state Zendesk rejects, verify the others still transition and the failed id is reportedticket_id_fieldand verify the host record's column is updated after ticket creation🤖 Generated with Claude Code
Note
Add smart action plugins for Zendesk ticket creation and closure
CloseTicketplugin that registers SINGLE and BULK scoped smart actions to mark Zendesk tickets assolvedorclosed, with per-id error handling and idempotent detection of already-closed tickets.CreateTicketWithNotificationplugin that registers a smart action to create a Zendesk ticket from a dynamic form, optionally notifying the requester via email and writing the created ticket id back to the host record.email_templatesare provided, with token interpolation ({{record.<field>}}) and HTML escaping for message content.TicketEnumsmodule consumed by both the schema and the new plugins.Macroscope summarized 8aca51b.