Files
mattermost/api/v4/source/scheduled_post.yaml
T
Nick MisasiandCursor Agent 9dfbaeca99 Add weekly recurring scheduled posts (#37746)
* Add weekly recurring scheduled posts.

Extend scheduled posts so users can schedule weekly repeats and keep the series healthy across sends, reschedules, and UI updates instead of falling back to one-shot behavior.

Made-with: Cursor

* Add Playwright coverage for recurring scheduled posts.

Cover weekly recurring scheduled messages in the scheduled-messages spec so the recurring UI and reschedule flow stay protected without adding a separate test surface.

Made-with: Cursor

* Fix recurring scheduled post CI failures.

Resolve the initial lint and formatting issues and renumber the new scheduled-post migration so it no longer collides with master during Postgres-backed test setup.

Made-with: Cursor

* Sync recurring scheduled post translation files.

Regenerate the affected English translation catalogs so the recurring scheduled post strings match the source extraction order expected by CI.

Made-with: Cursor

* Fix recurring scheduled post review follow-ups.

Preserve overdue cleanup behavior during weekly catch-up, defer delete websocket events until deletion succeeds, and address the remaining migration and UI review nits.

Made-with: Cursor

* Address recurring scheduled post review feedback

Made-with: Cursor

* Fix recurring scheduled post CI checks

Made-with: Cursor

* Make pending scheduled post keyset cursor index-scannable

EXPLAIN ANALYZE on a 5M-row ScheduledPosts table showed the pure OR-form
cursor forced Postgres to scan idx_scheduledposts_pending_scheduled_at_id
from the top on every page (~383ms/page, ~2M rows filtered). Keeping the
ScheduledAt <= beforeTime bound outside the tie-break restores the index
boundary (~0.12ms/page). Adds storetest coverage for cursor pagination.

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Address code quality review findings for recurring scheduled posts

Server:
- Replace silent-fallback AdvanceWeeklyScheduledNextOccurrence with
  error-returning ScheduledPost.ComputeNextScheduledAt; a recurring post
  whose timezone fails to load is now routed through the failed-post path
  instead of being reposted every job run or silently deleted
- Add ScheduledPost.IsRecurring and partition batches in
  processScheduledPostBatch; advance/delete now run independently so a
  store failure in one path can't cause reposts in the other
- Drop dead generality in UpdateRecurringScheduledPosts (only ScheduledAt
  varies per row; ErrorCode/ProcessedAt are constants)
- Simplify redundant repeat-type condition in GetPendingScheduledPosts

Webapp:
- Add shared isRecurringScheduledPost helper, replacing six scattered
  repeat_type === 'weekly' literals
- Recurrence timezone is now simply the scheduler's current timezone;
  removes initialRepeatTimezone/effectiveTimezone plumbing and the
  modal-label/picker timezone mismatch
- Single enforcement point for hiding send-now on recurring posts
- Use canonical getTeamIdByChannelId at both SCHEDULED_POST_UPDATED
  dispatch sites; rewrite errorsByTeamId update case as remove-then-add
  and add reducer tests

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Simplify recurring scheduled post code per review

- End a recurring series when its channel no longer exists instead of
  advancing it forever (matches the one-shot channel-not-found handling)
- Collapse the errorsByTeamId SCHEDULED_POST_UPDATED case into the
  identical SINGLE_SCHEDULED_POST_RECEIVED case (a scheduled post's team
  can't change) and combine duplicate byId cases; preserves state
  references on no-op updates
- Drop no-op timezone conversions in ComputeNextScheduledAt
- Remove redundant checkbox aria-label (label htmlFor already names it)
- Schedule new job tests in the past instead of sleeping a real second
- Remove redundant test assignment and e2e positional boolean

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Remove slop from recurring scheduled post changes

- Drop the business-rule CHECK constraint from the recurrence migration;
  no other migration enforces model-layer validation in the database,
  and BaseIsValid plus the job's failure handling already own it
- Fold the standalone repeat-validation test file into the existing
  TestScheduledPostBaseIsValid, matching its conventions
- Revert unrelated benchmark modernization in utils_test.go
- Drop an unneeded cast, unused fixture fields, and naming/assertion
  inconsistencies in tests

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Regenerate ScheduledPostStore mock with mockery ordering

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Address CodeRabbit review feedback

- Reject the host-dependent 'Local' value for RepeatTimezone; recurring
  schedules need a fixed zone (UTC or IANA name)
- Hide reschedule for deactivated DMs, matching send-now eligibility

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Resolve scheduled post team bucket from existing state when channel is unloaded

An admin can reschedule before fetchMissingChannels resolves, in which case
deriving the team from the channel returns undefined and the update was
misfiled under directChannels. getScheduledPostTeamId falls back to the
byTeamId bucket that already holds the post.

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Retrigger CI after transient enterprise npm network failure

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Clear hover/focus before asserting scheduled post header details in e2e

The drafts panel hides its timestamp/tag info section while hovered or
focus-within; after the reschedule modal closes, focus returns to the row
and the 'Repeats weekly' tag assertion saw a hidden element.

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Move recurrence columns into baseColumns and dispatch ComputeNextScheduledAt on repeat type

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Preserve recurrence when scheduled post updates omit repeat fields

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Disallow file attachments on recurring scheduled posts

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Stop pinning the channel indicator for recurring-only scheduled posts

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Re-home nullable-Type comment onto baseColumns

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Gate recurring scheduled posts behind a default-off feature flag

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Move presence-preservation rationale to the copy site

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Extract draftHasAttachments helper and require allowRecurring at single-caller layers

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Return null from the indicator selector when nothing should show

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Gate only recurrence transitions and preserve existing series in the modal

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Avoid err shadowing in scheduled post update handler

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Disable the repeat weekly checkbox with a tooltip when the message has attachments

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Return an explicit disposition from postScheduledPost

The batch loop inferred 'channel permanently gone' from the one return
path with a nil error and an error code set - an invariant a future
change could silently break, deleting recurring series by accident.
postScheduledPost now returns posted/failed/unsendable explicitly, the
switch refuses to delete on an unhandled disposition, and a test pins
that both recurring and one-shot posts in a nonexistent channel are
permanently deleted.

Co-authored-by: nick.misasi <nick.misasi@mattermost.com>

* Fix Repeat weekly attachments tooltip centering on the modal row (#37927)

Shrink-wrap the repeat checkbox row so WithTooltip anchors to the
control/label instead of the full modal body width.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-08-13 07:27:23 -04:00

216 lines
7.0 KiB
YAML

/api/v4/posts/schedule:
post:
tags:
- scheduled_post
summary: Creates a scheduled post
description: >
Creates a scheduled post
##### Permissions
Must have `create_post` permission for the channel the post is being created in.
__Minimum server version__: 10.3
operationId: CreateScheduledPost
requestBody:
content:
application/json:
schema:
type: object
required:
- channel_id
- message
- scheduled_at
properties:
scheduled_at:
type: integer
description: UNIX timestamp in milliseconds of the time when the scheduled post should be sent
channel_id:
type: string
description: The channel ID to post in
message:
type: string
description: The message contents, can be formatted with Markdown
root_id:
type: string
description: The post ID to comment on
file_ids:
type: array
description: A list of file IDs to associate with the post. Note that
posts are limited to 5 files maximum. Please use additional
posts for more files.
props:
description: A general JSON property bag to attach to the post
type: object
repeat_type:
type: string
description: Set to `weekly` for a recurring weekly schedule, or omit for a one-time schedule
repeat_timezone:
type: string
description: IANA timezone for weekly recurrence; required when `repeat_type` is `weekly`
responses:
"200":
description: Created scheduled post
content:
application/json:
schema:
type: object
items:
$ref: "#/components/schemas/ScheduledPost"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalServerError"
/api/v4/posts/scheduled/team/{team_id}:
get:
tags:
- scheduled_post
summary: Gets all scheduled posts for a user for the specified team..
description: >
Get user-team scheduled posts
##### Permissions
Must have `view_team` permission for the team the scheduled posts are being fetched for.
__Minimum server version__: 10.3
operationId: GetUserScheduledPosts
parameters:
- name: includeDirectChannels
in: query
description: Whether to include scheduled posts from DMs an GMs or not. Default is false
required: false
schema:
type: boolean
default: false
responses:
"200":
description: Created scheduled post
content:
application/json:
schema:
type: object
additionalProperties:
type: array
items:
$ref: "#/components/schemas/ScheduledPost"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalServerError"
/api/v4/posts/schedule/{scheduled_post_id}:
put:
tags:
- scheduled_post
summary: Update a scheduled post
description: >
Updates a scheduled post
##### Permissions
Must have `create_post` permission for the channel where the scheduled post belongs to.
__Minimum server version__: 10.3
operationId: UpdateScheduledPost
parameters:
- name: scheduled_post_id
in: path
description: ID of the scheduled post to update
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: object
required:
- id
- channel_id
- user_id
- message
- scheduled_at
properties:
id:
description: ID of the scheduled post to update
type: string
channel_id:
type: string
description: The channel ID to post in
user_id:
type: string
description: The current user ID
scheduled_at:
type: integer
description: UNIX timestamp in milliseconds of the time when the scheduled post should be sent
message:
type: string
description: The message contents, can be formatted with Markdown
repeat_type:
type: string
description: Set to `weekly` for a recurring weekly schedule, or empty for a one-time schedule
repeat_timezone:
type: string
description: IANA timezone for weekly recurrence; required when `repeat_type` is `weekly`
responses:
"200":
description: Updated scheduled post
content:
application/json:
schema:
type: object
items:
$ref: "#/components/schemas/ScheduledPost"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalServerError"
delete:
tags:
- scheduled_post
summary: Delete a scheduled post
description: >
Delete a scheduled post
__Minimum server version__: 10.3
operationId: DeleteScheduledPost
parameters:
- name: scheduled_post_id
in: path
description: ID of the scheduled post to delete
required: true
schema:
type: string
responses:
"200":
description: Deleted scheduled post
content:
application/json:
schema:
type: object
items:
$ref: "#/components/schemas/ScheduledPost"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalServerError"