Skip to content

Commit 707bde6

Browse files
authored
Merge branch 'LordNayan:main' into main
2 parents 89f593e + 655b175 commit 707bde6

12 files changed

Lines changed: 1378 additions & 6 deletions

File tree

apps/api/src/controllers/changelog.controller.ts

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,18 @@ async function formatChangelog(entry: any) {
1313
? await Post.find({ _id: { $in: entry.postIDs } })
1414
: [];
1515

16+
const markdownDetails = entry.markdownDetails || '';
17+
const plaintextDetails = entry.plaintextDetails || (markdownDetails ? htmlToPlainText(markdownDetails) : '');
18+
1619
return {
1720
id: entry._id.toString(),
1821
created: entry.created?.toISOString(),
1922
labels: entry.labels || [],
2023
lastSavedAt: entry.lastSavedAt?.toISOString(),
21-
markdownDetails: entry.markdownDetails || '',
22-
plaintextDetails: entry.plaintextDetails || '',
24+
markdownDetails,
25+
plaintextDetails,
26+
// description is kept for compatibility with existing frontends
27+
description: plaintextDetails,
2328
posts: posts.map(p => ({
2429
id: p._id.toString(),
2530
title: p.title,
@@ -179,6 +184,94 @@ export const createChangelog = asyncHandler(async (req: Request, res: Response):
179184
res.json(formatted);
180185
});
181186

187+
/**
188+
* POST /entries/update
189+
* Update an existing changelog entry
190+
*/
191+
export const updateChangelog = asyncHandler(async (req: Request, res: Response): Promise<void> => {
192+
const { id, title, details, labels, types, postIDs, publish } = req.body;
193+
194+
if (!mongoose.Types.ObjectId.isValid(id)) {
195+
throw new AppError('invalid entry id', 400);
196+
}
197+
198+
// Determine company context and load existing entry
199+
let companyID: mongoose.Types.ObjectId;
200+
201+
if (req.company) {
202+
companyID = req.company._id;
203+
} else if ((req as any).user?.companyID) {
204+
companyID = (req as any).user.companyID;
205+
} else {
206+
const existing = await Changelog.findById(id);
207+
if (!existing) {
208+
throw new AppError('entry not found', 404);
209+
}
210+
companyID = existing.companyID;
211+
}
212+
213+
const existing = await Changelog.findOne({ _id: id, companyID });
214+
if (!existing) {
215+
throw new AppError('entry not found', 404);
216+
}
217+
218+
// Validate and map post IDs if provided
219+
let validPostIDs: mongoose.Types.ObjectId[] | undefined;
220+
if (postIDs && postIDs.length > 0) {
221+
const posts = await Post.find({ _id: { $in: postIDs }, companyID });
222+
validPostIDs = posts.map(p => p._id);
223+
}
224+
225+
const update: any = { lastSavedAt: new Date() };
226+
227+
if (typeof title === 'string') {
228+
update.title = title;
229+
}
230+
231+
if (typeof details === 'string') {
232+
update.markdownDetails = details;
233+
update.plaintextDetails = htmlToPlainText(details || '');
234+
}
235+
236+
if (Array.isArray(labels)) {
237+
update.labels = labels.map((label: string, index: number) => ({
238+
id: `label-${index}`,
239+
name: label,
240+
}));
241+
}
242+
243+
if (Array.isArray(types)) {
244+
update.types = types;
245+
}
246+
247+
if (validPostIDs) {
248+
update.postIDs = validPostIDs;
249+
}
250+
251+
if (typeof publish === 'boolean') {
252+
if (publish && existing.status !== 'published') {
253+
update.status = 'published';
254+
update.publishedAt = new Date();
255+
} else if (!publish && existing.status === 'published') {
256+
update.status = 'draft';
257+
update.publishedAt = undefined;
258+
}
259+
}
260+
261+
const updated = await Changelog.findOneAndUpdate(
262+
{ _id: id, companyID },
263+
update,
264+
{ new: true }
265+
);
266+
267+
if (!updated) {
268+
throw new AppError('failed to update entry', 500);
269+
}
270+
271+
const formatted = await formatChangelog(updated);
272+
res.json(formatted);
273+
});
274+
182275
/**
183276
* POST /entries/delete
184277
* Delete a changelog entry

apps/api/src/middlewares/validate.middleware.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,15 @@ export const changelogCreateSchema = z.object({
242242
postIDs: z.array(objectIdSchema).optional(),
243243
notify: z.boolean().default(false).optional(),
244244
});
245+
246+
export const changelogUpdateSchema = z.object({
247+
apiKey: z.string().optional(),
248+
id: objectIdSchema,
249+
title: z.string().min(1).max(500).optional(),
250+
details: z.string().max(100000).optional(),
251+
labels: z.array(z.string()).optional(),
252+
types: z.array(z.enum(['new', 'improved', 'fixed'])).optional(),
253+
postIDs: z.array(objectIdSchema).optional(),
254+
// When true, mark as published (and set publishedAt); when false, mark as draft
255+
publish: z.boolean().optional(),
256+
});

apps/api/src/routes/v1/changelog.routes.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Router } from 'express';
2-
import { optionalSubdomainAuth, flexibleAuth, validate, changelogListSchema, changelogCreateSchema } from '../../middlewares/index.js';
3-
import { listChangelog, retrieveChangelog, createChangelog, deleteChangelog } from '../../controllers/index.js';
2+
import { optionalSubdomainAuth, flexibleAuth, validate, changelogListSchema, changelogCreateSchema, changelogUpdateSchema } from '../../middlewares/index.js';
3+
import { listChangelog, retrieveChangelog, createChangelog, updateChangelog, deleteChangelog } from '../../controllers/index.js';
44

55
const router = Router();
66

@@ -19,6 +19,9 @@ router.post('/retrieve', retrieveChangelog);
1919
// POST /entries/create - Create a changelog entry
2020
router.post('/create', validate(changelogCreateSchema), createChangelog);
2121

22+
// POST /entries/update - Update a changelog entry
23+
router.post('/update', validate(changelogUpdateSchema), updateChangelog);
24+
2225
// POST /entries/delete - Delete a changelog entry
2326
router.post('/delete', deleteChangelog);
2427

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"react": "^18.2.0",
3737
"react-dom": "^18.2.0",
3838
"react-hot-toast": "^2.6.0",
39+
"react-markdown": "^9.0.0",
3940
"react-router-dom": "^7.13.0",
4041
"uuid": "^9.0.1",
4142
"zustand": "^4.4.7"

apps/web/src/App.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
PostDetailPage,
1414
AdminBoardsPage,
1515
AdminSettingsPage,
16+
AdminChangelogPage,
1617
} from './pages';
1718

1819
// Components
@@ -115,6 +116,17 @@ function AppRoutes() {
115116
}
116117
/>
117118

119+
<Route
120+
path="/admin/changelog"
121+
element={
122+
<ProtectedRoute requireAdmin>
123+
<Layout>
124+
<AdminChangelogPage />
125+
</Layout>
126+
</ProtectedRoute>
127+
}
128+
/>
129+
118130
<Route
119131
path="/admin/settings"
120132
element={

apps/web/src/components/shared/Layout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export function Layout({ children }: LayoutProps) {
3131
...(user?.isAdmin
3232
? [
3333
{ name: 'Boards', href: '/admin/boards', icon: Squares2X2Icon },
34+
{ name: 'Changelog', href: '/admin/changelog', icon: Squares2X2Icon },
3435
{ name: 'Settings', href: '/admin/settings', icon: Cog6ToothIcon },
3536
]
3637
: []),

0 commit comments

Comments
 (0)