Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

- Synchronize email-category and template browse-group contracts with app
revision `9bf2e01f14d8260bdde0825e901a05497b66c70c`. Template reads accept
`--group`, and `--count --group-by group` labels browse groups correctly in
human output. These additions require the matching API deployment.

## 0.17.0

Requires `@brew.new/sdk` `^13.0.0`. This is a minor release because it adds
Expand Down
4 changes: 3 additions & 1 deletion docs/commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2408,9 +2408,10 @@ List public templates (each row carries the rendered html)
- SDK: `brew.templates.list(...)`
- `--brand-name <name>` — Filter by gallery brand name
- `--category <category>` — Filter by category
- `--group <group>` — Filter by category browse group
- `--semantic <text>` — Semantic search over the gallery
- `--count` — Return matching template counts instead of rows
- `--group-by <field>` — Group counts by brand or category (requires --count)
- `--group-by <field>` — Group counts by brand, category or group (requires --count)
- `--limit <n>` — Page size, 1-100 (default 100)
- `--cursor <cursor>` — Opaque pagination cursor from a previous page
- `--all` — Follow the cursor and return every page as one result
Expand All @@ -2419,6 +2420,7 @@ List public templates (each row carries the rendered html)
```bash
brew-cli templates list --category welcome
brew-cli templates list --count --group-by category --json
brew-cli templates list --group events --count --group-by group --json
brew-cli templates list --semantic "minimal product launch" --json
```

Expand Down
66 changes: 47 additions & 19 deletions openapi/public-api-v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5208,30 +5208,36 @@ components:
type: string
enum:
- welcome
- onboarding
- newsletter
- promotional
- content
- event-invitation
- event-reminder
- event-recap
- product-launch
- product-update
- product-pitch
- promotional
- cart-abandonment
- re-engagement
- subscription
- feedback-request
- referral
- company-news
- fundraising
- order-confirmation
- shipping-update
- receipt
- cart-abandonment
- subscription
- password-reset
- verification
- security-alert
- account-update
- event-invitation
- event-reminder
- feedback-request
- re-engagement
- referral
- notification
- support
- business
- internal
- notification
- business
- general
description: "Email category that steers the design treatment (exemplars, hero recipe, personalization) — mirrors what the in-app agent infers per request. One of: welcome, newsletter, promotional, product-launch, product-update, order-confirmation, shipping-update, receipt, cart-abandonment, subscription, password-reset, verification, security-alert, account-update, event-invitation, event-reminder, feedback-request, re-engagement, referral, support, business, internal, notification, general. Omit for a general treatment. Transactional categories (receipt, password-reset, order-confirmation, …) steer receipt/reset design conventions; to DELIVER those emails, wire the design into an automation with a trigger and a transactional-purpose sending domain, then fire the trigger."
description: "Email category that steers the design treatment (exemplars, hero recipe, personalization) — mirrors what the in-app agent infers per request. One of: welcome, onboarding, newsletter, content, event-invitation, event-reminder, event-recap, product-launch, product-update, product-pitch, promotional, cart-abandonment, re-engagement, subscription, feedback-request, referral, company-news, fundraising, order-confirmation, shipping-update, receipt, password-reset, verification, security-alert, account-update, notification, support, internal, business, general. Omit for a general treatment. Transactional categories (receipt, password-reset, order-confirmation, …) steer receipt/reset design conventions; to DELIVER those emails, wire the design into an automation with a trigger and a transactional-purpose sending domain, then fire the trigger."
subjectLine:
type: string
minLength: 1
Expand Down Expand Up @@ -58713,7 +58719,7 @@ paths:
tags:
- Templates
summary: List templates
description: Lists public templates under { data, pagination }. The default full representation includes HTML. Set representation=summary for metadata, previewImage, viewUrl, and referenceEmailId without HTML. Supports exact brand and category filters; query matches title text or an exact template identifier. semantic ranks up to 200 matches using vector search; query additionally filters those matches when both are supplied. Browse defaults to 50 full rows or 20 summary rows. Brand and category match case-insensitively. Continue with pagination.cursor, including after an empty filtered page with hasMore=true. Templates are organization-wide references; pass referenceEmailId to POST /v1/emails to remix one. Set count=true for { count }, the number of templates matching brand and category (refreshed every 15 minutes), instead of rows; add groupBy=brand or groupBy=category for per-value counts, largest first, 100 groups a page with the same pagination.cursor (count does not combine with semantic or query).
description: "Lists public templates under { data, pagination }. The default full representation includes HTML. Set representation=summary for metadata, previewImage, viewUrl, and referenceEmailId without HTML. Supports exact brand and category filters and a group filter (a browse group: every category in it); query matches title text or an exact template identifier. semantic ranks up to 200 matches using vector search; query additionally filters those matches when both are supplied. Browse defaults to 50 full rows or 20 summary rows. Brand and category match case-insensitively. Continue with pagination.cursor, including after an empty filtered page with hasMore=true. Templates are organization-wide references; pass referenceEmailId to POST /v1/emails to remix one. Set count=true for { count }, the number of templates matching brand, category and group (refreshed every 15 minutes), instead of rows; add groupBy=brand, groupBy=category or groupBy=group for per-value counts, largest first, 100 groups a page with the same pagination.cursor (count does not combine with semantic or query)."
parameters:
- schema:
type: string
Expand All @@ -58726,11 +58732,30 @@ paths:
- schema:
type: string
minLength: 1
description: "Email category, matched case-insensitively (unknown → empty page): welcome, newsletter, promotional, product-launch, product-update, order-confirmation, shipping-update, receipt, cart-abandonment, subscription, password-reset, verification, security-alert, account-update, event-invitation, event-reminder, feedback-request, re-engagement, referral, support, business, internal, notification, general."
description: "Email category, matched case-insensitively (unknown → empty page): welcome, onboarding, newsletter, content, event-invitation, event-reminder, event-recap, product-launch, product-update, product-pitch, promotional, cart-abandonment, re-engagement, subscription, feedback-request, referral, company-news, fundraising, order-confirmation, shipping-update, receipt, password-reset, verification, security-alert, account-update, notification, support, internal, business, general."
required: false
description: "Email category, matched case-insensitively (unknown → empty page): welcome, newsletter, promotional, product-launch, product-update, order-confirmation, shipping-update, receipt, cart-abandonment, subscription, password-reset, verification, security-alert, account-update, event-invitation, event-reminder, feedback-request, re-engagement, referral, support, business, internal, notification, general."
description: "Email category, matched case-insensitively (unknown → empty page): welcome, onboarding, newsletter, content, event-invitation, event-reminder, event-recap, product-launch, product-update, product-pitch, promotional, cart-abandonment, re-engagement, subscription, feedback-request, referral, company-news, fundraising, order-confirmation, shipping-update, receipt, password-reset, verification, security-alert, account-update, notification, support, internal, business, general."
name: category
in: query
- schema:
type: string
enum:
- welcome-onboarding
- newsletter
- content
- events
- product-news
- sales
- retention
- company
- transactional
- internal
- other
description: "A browse group: every category in it (e.g. `events` = invitations, reminders and recaps)."
required: false
description: "A browse group: every category in it (e.g. `events` = invitations, reminders and recaps)."
name: group
in: query
- schema:
type: string
minLength: 1
Expand Down Expand Up @@ -58766,22 +58791,24 @@ paths:
enum:
- 'true'
- 'false'
description: Return `{ count }`, the number of templates matching `brand`/`category`, instead of rows. Not with `semantic` or `query`.
description: Return `{ count }`, the number of templates matching `brand`/`category`/`group`, instead of rows. Not with `semantic` or `query`.
required: false
description: Return `{ count }`, the number of templates matching `brand`/`category`, instead of rows. Not with `semantic` or `query`.
description: Return `{ count }`, the number of templates matching `brand`/`category`/`group`, instead of rows. Not with `semantic` or `query`.
name: count
in: query
- schema:
type: string
enum:
- brand
- category
description: "With `count: true`: also count per brand or per category, largest first, 100 groups a page; continue with `cursor`."
- group
description: "With `count: true`: also count per brand, category or group, largest first, 100 groups a page; continue with `cursor`."
x-brew-group-by-tokens:
- brand
- category
- group
required: false
description: "With `count: true`: also count per brand or per category, largest first, 100 groups a page; continue with `cursor`."
description: "With `count: true`: also count per brand, category or group, largest first, 100 groups a page; continue with `cursor`."
name: groupBy
in: query
- schema:
Expand Down Expand Up @@ -58912,6 +58939,7 @@ paths:
enum:
- brand
- category
- group
groups:
type: array
items:
Expand All @@ -58920,7 +58948,7 @@ paths:
value:
type: string
minLength: 1
description: The brand domain or category; pass it back as the `brand`/`category` filter.
description: The brand domain, category or group; pass it back as the filter of the same name (`brand`, `category` or `group`).
name:
type: string
minLength: 1
Expand Down
34 changes: 22 additions & 12 deletions src/commands/templates/list.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import type {
ListTemplateRowsInput,
ListTemplatesInput,
TemplatesCountResponse,
} from '@brew.new/sdk'
import type { ListTemplateRowsInput, ListTemplatesInput } from '@brew.new/sdk'
import type { operations } from '../../generated/openapi-types'
import { defineCommand } from '../../lib/define-command'
import { CliUsageError } from '../../lib/errors'
import {
Expand All @@ -21,6 +18,17 @@ import {
LIMIT_FLAG,
} from '../../lib/paginate'

type TemplatesCountResponse = Extract<
operations['listTemplates']['responses'][200]['content']['application/json'],
{ count: number }
>

const GROUP_LABELS = {
brand: { header: 'BRAND', noun: 'brands', singular: 'brand' },
category: { header: 'CATEGORY', noun: 'categories', singular: 'category' },
group: { header: 'GROUP', noun: 'groups', singular: 'group' },
} as const

export const templatesListCommand = defineCommand({
path: ['templates', 'list'],
summary: 'List public templates (each row carries the rendered html)',
Expand All @@ -34,6 +42,7 @@ export const templatesListCommand = defineCommand({
summary: 'Filter by gallery brand name',
},
{ flag: '--category <category>', summary: 'Filter by category' },
{ flag: '--group <group>', summary: 'Filter by category browse group' },
{
flag: '--semantic <text>',
summary: 'Semantic search over the gallery',
Expand All @@ -44,7 +53,7 @@ export const templatesListCommand = defineCommand({
},
{
flag: '--group-by <field>',
summary: 'Group counts by brand or category (requires --count)',
summary: 'Group counts by brand, category or group (requires --count)',
},
LIMIT_FLAG,
CURSOR_FLAG,
Expand All @@ -54,13 +63,15 @@ export const templatesListCommand = defineCommand({
examples: [
'brew-cli templates list --category welcome',
'brew-cli templates list --count --group-by category --json',
'brew-cli templates list --group events --count --group-by group --json',
'brew-cli templates list --semantic "minimal product launch" --json',
],
run: async ({ ctx, flags }) => {
const base = await readJsonFlag(ctx, flags.input, '--input')
const input = mergeInput(base, {
brand: flagString(flags.brandName),
category: flagString(flags.category),
group: flagString(flags.group),
semantic: flagString(flags.semantic),
count: flags.count === true ? true : undefined,
groupBy: flagString(flags.groupBy),
Expand Down Expand Up @@ -113,13 +124,13 @@ function renderCount(body: TemplatesCountResponse): string {
if (body.groupBy === undefined && groups.length === 0) {
return total
}
const noun = body.groupBy === 'category' ? 'categories' : 'brands'
const { header, noun, singular } = GROUP_LABELS[body.groupBy ?? 'brand']
const table = renderTable(
groups.map((group) => ({ ...group })),
[
{
key: 'value',
header: body.groupBy === 'category' ? 'CATEGORY' : 'BRAND',
header,
},
...(groups.some((group) => group.name !== undefined)
? [{ key: 'name', header: 'NAME' }]
Expand All @@ -128,12 +139,11 @@ function renderCount(body: TemplatesCountResponse): string {
]
)
const cursor = body.pagination?.hasMore ? body.pagination.cursor : null
const groupNoun = body.groupCount === 1 ? singular : noun
const footer = [
`${total} in total`,
body.groupCount === undefined ? '' : `${body.groupCount} ${noun}`,
body.ungroupedCount
? `${body.ungroupedCount} with no ${noun === 'brands' ? 'brand' : 'category'}`
: '',
body.groupCount === undefined ? '' : `${body.groupCount} ${groupNoun}`,
body.ungroupedCount ? `${body.ungroupedCount} with no ${singular}` : '',
cursor === null ? '' : `more groups: --cursor ${cursor}`,
]
.filter(Boolean)
Expand Down
Loading
Loading