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:
@@ -112,6 +112,7 @@ export const ReadModels = {
|
||||
TreeFollow: 'TreeFollow',
|
||||
IssueReminder: 'IssueReminder',
|
||||
MyDay: 'MyDay',
|
||||
ReleaseNotesShare: 'ReleaseNotesShare',
|
||||
Sprint: 'SprintReadModel',
|
||||
GitIntegration: 'GitIntegration',
|
||||
OutgoingWebhook: 'OutgoingWebhook',
|
||||
@@ -395,6 +396,15 @@ class MyDayDocument {
|
||||
export const MyDaySchema = SchemaFactory.createForClass(MyDayDocument);
|
||||
MyDaySchema.index({ userId: 1, day: -1 }, { unique: true });
|
||||
|
||||
@Schema({ ...LOOSE, collection: 'release_note_shares' })
|
||||
class ReleaseNotesShareDocument {
|
||||
@Prop({ type: String, required: true, unique: true }) releaseId!: string;
|
||||
@Prop({ type: String, required: true, unique: true }) token!: string;
|
||||
}
|
||||
export const ReleaseNotesShareSchema = SchemaFactory.createForClass(
|
||||
ReleaseNotesShareDocument,
|
||||
);
|
||||
|
||||
@Schema({ ...LOOSE, collection: 'git_integrations' })
|
||||
class GitIntegrationDocument {
|
||||
/** The project's id. */
|
||||
@@ -503,6 +513,7 @@ export const READ_MODEL_SCHEMAS = [
|
||||
{ name: ReadModels.TreeFollow, schema: TreeFollowSchema },
|
||||
{ name: ReadModels.IssueReminder, schema: IssueReminderSchema },
|
||||
{ name: ReadModels.MyDay, schema: MyDaySchema },
|
||||
{ name: ReadModels.ReleaseNotesShare, schema: ReleaseNotesShareSchema },
|
||||
{ name: ReadModels.CalendarFeed, schema: CalendarFeedSchema },
|
||||
{ name: ReadModels.DashboardShare, schema: DashboardShareSchema },
|
||||
{ name: ReadModels.PushSubscription, schema: PushSubscriptionSchema },
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import {
|
||||
ReleaseNoteSharesRepository,
|
||||
ReleaseNotesShare,
|
||||
} from '@task-stream/api/tracker/application';
|
||||
import { Model } from 'mongoose';
|
||||
import { ReadModels } from './read-model.schemas';
|
||||
|
||||
type Stored = ReleaseNotesShare & { _id?: unknown };
|
||||
|
||||
const strip = ({ _id: _ignored, ...share }: Stored): ReleaseNotesShare => share;
|
||||
|
||||
@Injectable()
|
||||
export class MongoReleaseNoteSharesRepository extends ReleaseNoteSharesRepository {
|
||||
constructor(
|
||||
@InjectModel(ReadModels.ReleaseNotesShare)
|
||||
private readonly shares: Model<Stored>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async findByRelease(releaseId: string): Promise<ReleaseNotesShare | null> {
|
||||
const doc = await this.shares.findOne({ releaseId }).lean<Stored>();
|
||||
return doc ? strip(doc) : null;
|
||||
}
|
||||
|
||||
async findByToken(token: string): Promise<ReleaseNotesShare | null> {
|
||||
const doc = await this.shares.findOne({ token }).lean<Stored>();
|
||||
return doc ? strip(doc) : null;
|
||||
}
|
||||
|
||||
async save(share: ReleaseNotesShare): Promise<void> {
|
||||
await this.shares.updateOne(
|
||||
{ releaseId: share.releaseId },
|
||||
{ $set: share },
|
||||
{ upsert: true },
|
||||
);
|
||||
}
|
||||
|
||||
async remove(releaseId: string): Promise<void> {
|
||||
await this.shares.deleteOne({ releaseId });
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
TreeFollowsRepository,
|
||||
IssueRemindersRepository,
|
||||
MyDaysRepository,
|
||||
ReleaseNoteSharesRepository,
|
||||
GitIntegrationsRepository,
|
||||
OutgoingWebhooksRepository,
|
||||
AutomationsRepository,
|
||||
@@ -88,6 +89,7 @@ import { MongoAlertIntegrationsRepository } from './mongo/alert-integrations.rep
|
||||
import { MongoTreeFollowsRepository } from './mongo/tree-follows.repository';
|
||||
import { MongoIssueRemindersRepository } from './mongo/issue-reminders.repository';
|
||||
import { MongoMyDaysRepository } from './mongo/my-days.repository';
|
||||
import { MongoReleaseNoteSharesRepository } from './mongo/release-note-shares.repository';
|
||||
import { MongoGitIntegrationsRepository } from './mongo/git-integrations.repository';
|
||||
import { MongoOutgoingWebhooksRepository } from './mongo/outgoing-webhooks.repository';
|
||||
import { HttpWebhookSender } from './webhooks/http-webhook.sender';
|
||||
@@ -175,6 +177,10 @@ const PORTS = [
|
||||
useClass: MongoIssueRemindersRepository,
|
||||
},
|
||||
{ provide: MyDaysRepository, useClass: MongoMyDaysRepository },
|
||||
{
|
||||
provide: ReleaseNoteSharesRepository,
|
||||
useClass: MongoReleaseNoteSharesRepository,
|
||||
},
|
||||
{
|
||||
provide: GitIntegrationsRepository,
|
||||
useClass: MongoGitIntegrationsRepository,
|
||||
|
||||
Reference in New Issue
Block a user