diff --git a/lib/api/index.js b/lib/api/index.js index 9135232..31fdfa6 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -23,6 +23,7 @@ import { router as searchRouter } from './routes/search.js'; import { router as jobsRouter } from './routes/jobs.js'; import { router as captureRouter } from './routes/capture.js'; import { spacesScopedRouter as companionRouter } from './routes/companion.js'; +import { router as dashboardRouter } from './routes/dashboard.js'; export function mountApi(app) { const api = Router(); @@ -53,6 +54,7 @@ export function mountApi(app) { api.use('/search', searchRouter); api.use('/jobs', jobsRouter); api.use('/capture', captureRouter); + api.use('/dashboard', dashboardRouter); api.use('/:entity_type/:entity_id/tags', tagsByEntityRouter); api.use((_req, _res, next) => next(new NotFoundError('route not found'))); diff --git a/lib/api/routes/dashboard.js b/lib/api/routes/dashboard.js new file mode 100644 index 0000000..142b5bd --- /dev/null +++ b/lib/api/routes/dashboard.js @@ -0,0 +1,26 @@ +import { Router } from 'express'; +import { z } from 'zod'; +import { validate } from '../validate.js'; +import { asyncWrap } from '../errors.js'; +import { requireOwner } from '../cap.js'; +import * as repo from '../../db/repos/dashboard_layout.js'; + +export const router = Router(); +router.use(requireOwner); + +const layoutSchema = z.object({ + card_order: z.array(z.string()).default([]), + hidden: z.array(z.string()).default([]), + sizes: z.record(z.enum(['s', 'm', 'l'])).default({}) +}); + +router.get('/layout', asyncWrap(async (_req, res) => { + res.json(await repo.get()); +})); + +router.put('/layout', + validate({ body: layoutSchema }), + asyncWrap(async (req, res) => { + res.json(await repo.put(req.body)); + }) +); diff --git a/tests/api/dashboard.test.js b/tests/api/dashboard.test.js new file mode 100644 index 0000000..cde5d07 --- /dev/null +++ b/tests/api/dashboard.test.js @@ -0,0 +1,34 @@ +import { describe, it, expect, beforeAll } from 'vitest'; +import request from 'supertest'; +import { setup } from './helpers.js'; + +let app, ownerHeaders; +beforeAll(async () => { ({ app, ownerHeaders } = await setup()); }); + +describe('dashboard layout api', () => { + it('401 without auth', async () => { + const res = await request(app).get('/api/dashboard/layout'); + expect(res.status).toBe(401); + }); + + it('GET returns defaults', async () => { + const res = await request(app).get('/api/dashboard/layout').set(ownerHeaders); + expect(res.status).toBe(200); + expect(res.body).toEqual({ card_order: [], hidden: [], sizes: {} }); + }); + + it('PUT persists and GET reflects it', async () => { + const body = { card_order: ['clock', 'weather'], hidden: [], sizes: { weather: 's' } }; + const put = await request(app).put('/api/dashboard/layout').set(ownerHeaders).send(body); + expect(put.status).toBe(200); + const get = await request(app).get('/api/dashboard/layout').set(ownerHeaders); + expect(get.body.card_order).toEqual(['clock', 'weather']); + expect(get.body.sizes).toEqual({ weather: 's' }); + }); + + it('PUT rejects a bad size value', async () => { + const res = await request(app).put('/api/dashboard/layout').set(ownerHeaders) + .send({ card_order: [], hidden: [], sizes: { weather: 'huge' } }); + expect(res.status).toBe(400); + }); +});