feat: a public notes page per release, for clients

On the releases page, «Página pública» (owners and admins) opens a
secret link to a page with the release's notes, readable without an
account, also for private projects: the notes written, or the draft
from its finished tickets while there are none (without repeating the
title the page already shows). The link can be copied, renewed (the old
one stops working) or withdrawn. Stored in Mongo (`release_note_shares`).

API: shareReleaseNotes / unshareReleaseNotes / releaseNotesShare,
publicReleaseNotes(token); GET/PUT/DELETE /api/releases/{id}/public-notes,
GET /api/shared/release-notes/{token}. Web: /notes/:token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 20:57:40 +02:00
parent 3a46a236e4
commit 8ed16f133e
27 changed files with 2158 additions and 6 deletions

View File

@@ -3010,6 +3010,57 @@ export type SharedDashboardQuery = {
} | null;
};
export type ReleaseNotesShareFieldsFragment = {
releaseId: string;
token: string;
createdAt: string;
};
export type ReleaseNotesShareQueryVariables = Exact<{
releaseId: string;
}>;
export type ReleaseNotesShareQuery = {
releaseNotesShare: {
releaseId: string;
token: string;
createdAt: string;
} | null;
};
export type ShareReleaseNotesMutationVariables = Exact<{
releaseId: string;
renew: boolean;
}>;
export type ShareReleaseNotesMutation = {
shareReleaseNotes: { releaseId: string; token: string; createdAt: string };
};
export type UnshareReleaseNotesMutationVariables = Exact<{
releaseId: string;
}>;
export type UnshareReleaseNotesMutation = { unshareReleaseNotes: boolean };
export type PublicReleaseNotesQueryVariables = Exact<{
token: string;
}>;
export type PublicReleaseNotesQuery = {
publicReleaseNotes: {
projectName: string;
projectKey: string;
releaseName: string;
description: string | null;
status: ReleaseStatus;
targetDate: string | null;
releasedAt: string | null;
notes: string;
written: boolean;
} | null;
};
export type SprintFieldsFragment = {
id: string;
projectId: string;
@@ -4824,6 +4875,27 @@ export const DashboardShareFieldsFragmentDoc = {
},
],
} as unknown as DocumentNode<DashboardShareFieldsFragment, unknown>;
export const ReleaseNotesShareFieldsFragmentDoc = {
kind: 'Document',
definitions: [
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'ReleaseNotesShareFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'ReleaseNotesShare' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'releaseId' } },
{ kind: 'Field', name: { kind: 'Name', value: 'token' } },
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
],
},
},
],
} as unknown as DocumentNode<ReleaseNotesShareFieldsFragment, unknown>;
export const SprintFieldsFragmentDoc = {
kind: 'Document',
definitions: [
@@ -18184,6 +18256,275 @@ export const SharedDashboardDocument = {
SharedDashboardQuery,
SharedDashboardQueryVariables
>;
export const ReleaseNotesShareDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
name: { kind: 'Name', value: 'ReleaseNotesShare' },
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'releaseId' },
},
type: {
kind: 'NonNullType',
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'releaseNotesShare' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'releaseId' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'releaseId' },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'ReleaseNotesShareFields' },
},
],
},
},
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'ReleaseNotesShareFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'ReleaseNotesShare' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'releaseId' } },
{ kind: 'Field', name: { kind: 'Name', value: 'token' } },
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
],
},
},
],
} as unknown as DocumentNode<
ReleaseNotesShareQuery,
ReleaseNotesShareQueryVariables
>;
export const ShareReleaseNotesDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'mutation',
name: { kind: 'Name', value: 'ShareReleaseNotes' },
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'releaseId' },
},
type: {
kind: 'NonNullType',
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'renew' },
},
type: {
kind: 'NonNullType',
type: {
kind: 'NamedType',
name: { kind: 'Name', value: 'Boolean' },
},
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'shareReleaseNotes' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'releaseId' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'releaseId' },
},
},
{
kind: 'Argument',
name: { kind: 'Name', value: 'renew' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'renew' },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'ReleaseNotesShareFields' },
},
],
},
},
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'ReleaseNotesShareFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'ReleaseNotesShare' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'releaseId' } },
{ kind: 'Field', name: { kind: 'Name', value: 'token' } },
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
],
},
},
],
} as unknown as DocumentNode<
ShareReleaseNotesMutation,
ShareReleaseNotesMutationVariables
>;
export const UnshareReleaseNotesDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'mutation',
name: { kind: 'Name', value: 'UnshareReleaseNotes' },
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'releaseId' },
},
type: {
kind: 'NonNullType',
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'unshareReleaseNotes' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'releaseId' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'releaseId' },
},
},
],
},
],
},
},
],
} as unknown as DocumentNode<
UnshareReleaseNotesMutation,
UnshareReleaseNotesMutationVariables
>;
export const PublicReleaseNotesDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
name: { kind: 'Name', value: 'PublicReleaseNotes' },
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'token' },
},
type: {
kind: 'NonNullType',
type: {
kind: 'NamedType',
name: { kind: 'Name', value: 'String' },
},
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'publicReleaseNotes' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'token' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'token' },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'projectName' } },
{ kind: 'Field', name: { kind: 'Name', value: 'projectKey' } },
{ kind: 'Field', name: { kind: 'Name', value: 'releaseName' } },
{ kind: 'Field', name: { kind: 'Name', value: 'description' } },
{ kind: 'Field', name: { kind: 'Name', value: 'status' } },
{ kind: 'Field', name: { kind: 'Name', value: 'targetDate' } },
{ kind: 'Field', name: { kind: 'Name', value: 'releasedAt' } },
{ kind: 'Field', name: { kind: 'Name', value: 'notes' } },
{ kind: 'Field', name: { kind: 'Name', value: 'written' } },
],
},
},
],
},
},
],
} as unknown as DocumentNode<
PublicReleaseNotesQuery,
PublicReleaseNotesQueryVariables
>;
export const SprintsDocument = {
kind: 'Document',
definitions: [