Shared Family calendar and task list; notes default to Family; phone chat and Family card fixes
build-container / build (push) Successful in 4m14s
build-container / build (push) Successful in 4m14s
- Built-in Family calendar everyone with Calendar can edit (data/family) - Family task list shared by everyone with Tasks - New notes and lists start shared with the family - Phone chat sized above the bottom bar and mini player so the message box is visible - Family page cards no longer overflow on long chat previews - Tests added and updated Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0383da87f5
commit
058f410a58
11 files changed
+65
-17
No files matched your search
+6
-6
@@ -24,7 +24,7 @@ const titles = async u => (await call(u, win)).json.events.map(e => e.title).sor
|
||||
|
||||
(async () => {
|
||||
let r = await call(ann, '/api/cal/list');
|
||||
ok('everyone starts with the one NE Hub calendar', r.json.length === 1 && r.json[0].id === 'local' && r.json[0].name === 'NE Hub' && r.json[0].color === '#4f7cff', r.json);
|
||||
ok('everyone starts with the one NE Hub calendar', r.json.length === 2 && r.json[0].id === 'local' && r.json[0].name === 'NE Hub' && r.json[1].id === 'family~local' && r.json[0].color === '#4f7cff', r.json);
|
||||
|
||||
// legacy events (no calendar field) belong to the first calendar
|
||||
fs.writeFileSync(path.join(users.dirOf(ann.id), 'events.json'), JSON.stringify([{ uid: 'old1', fields: F('Old event'), created: 1, updated: 1 }]));
|
||||
@@ -32,7 +32,7 @@ const titles = async u => (await call(u, win)).json.events.map(e => e.title).sor
|
||||
|
||||
// create, recolor, rename
|
||||
r = await call(ann, '/api/cal/ncal', { name: ' Family ', color: '#d9453a' }); const fam = r.json.id;
|
||||
ok('a person can make another calendar', r.status === 200 && fam && (await call(ann, '/api/cal/list')).json.length === 2);
|
||||
ok('a person can make another calendar', r.status === 200 && fam && (await call(ann, '/api/cal/list')).json.filter(c => c.id !== 'family~local').length === 2);
|
||||
r = await call(ann, '/api/cal/ncal', { name: ' ' }); ok('a calendar needs a name', r.status === 400, r);
|
||||
r = await call(ann, '/api/cal/ncal/update', { id: fam, color: 'red' }); ok('colors must be real hex colors', r.status === 400, r);
|
||||
await call(ann, '/api/cal/ncal/update', { id: fam, name: 'Family stuff', color: '#8a5cf0' });
|
||||
@@ -48,7 +48,7 @@ const titles = async u => (await call(u, win)).json.events.map(e => e.title).sor
|
||||
ok('the same uid is not found in a different calendar', (await call(ann, `/api/cal/event?cal=local&uid=${dn.uid}&tz=UTC`)).status === 400);
|
||||
|
||||
// private by default
|
||||
ok('other people see nothing of it by default', (await titles(bob)) === '' && (await call(bob, '/api/cal/list')).json.length === 1);
|
||||
ok('other people see nothing of it by default', (await titles(bob)) === '' && (await call(bob, '/api/cal/list')).json.filter(c => c.id !== 'family~local').length === 1);
|
||||
r = await call(bob, '/api/cal/event', { cal: `${ann.id}~${fam}`, fields: F('Sneaky') }); ok('and cannot write to it', r.status === 400, r);
|
||||
r = await call(bob, `/api/cal/event?cal=${ann.id}~${fam}&uid=${dn.uid}&tz=UTC`); ok('or read it', r.status === 400, r);
|
||||
|
||||
@@ -57,7 +57,7 @@ const titles = async u => (await call(u, win)).json.events.map(e => e.title).sor
|
||||
r = await call(ann, '/api/cal/ncal/update', { id: fam, shares: [{ user: bob.id, mode: 'view' }, { user: cy.id, mode: 'edit' }, { user: ann.id, mode: 'edit' }, { user: 'nobody', mode: 'edit' }] });
|
||||
ok('sharing saves, ignoring yourself and unknown people', r.status === 200 && (await call(ann, '/api/cal/list')).json.find(c => c.id === fam).shares.length === 2);
|
||||
ok('a view-only person sees the events', (await titles(bob)) === 'Dinner');
|
||||
const bl = (await call(bob, '/api/cal/list')).json.find(c => c.shared);
|
||||
const bl = (await call(bob, '/api/cal/list')).json.find(c => c.shared && c.id !== 'family~local');
|
||||
ok('and the calendar in their list, marked shared with the owner and their access', bl && bl.mode === 'view' && bl.owner === 'Ann' && bl.name === 'Family stuff', bl);
|
||||
const bev = (await call(bob, win)).json.events[0]; ok('shared events say they cannot be edited', bev.editable === false && bev.shared === true, bev);
|
||||
r = await call(bob, '/api/cal/event', { cal: `${ann.id}~${fam}`, fields: F('Bob adds') }); ok('view-only people cannot add', r.status === 400 && /view only/.test(r.json.error), r);
|
||||
@@ -76,7 +76,7 @@ const titles = async u => (await call(u, win)).json.events.map(e => e.title).sor
|
||||
|
||||
// stopping sharing
|
||||
await call(ann, '/api/cal/ncal/update', { id: fam, shares: [{ user: cy.id, mode: 'edit' }] });
|
||||
ok('removing someone hides it from them right away', (await titles(bob)) === '' && (await call(bob, '/api/cal/list')).json.length === 1);
|
||||
ok('removing someone hides it from them right away', (await titles(bob)) === '' && (await call(bob, '/api/cal/list')).json.filter(c => c.id !== 'family~local').length === 1);
|
||||
|
||||
// subscribe links
|
||||
const feedOf = async (u, id, reset) => { const x = await call(u, '/api/cal/ncal/feed', { id, reset }); return x.json.url; };
|
||||
@@ -91,7 +91,7 @@ const titles = async u => (await call(u, win)).json.events.map(e => e.title).sor
|
||||
ok('making a new link kills the old one', u4 !== u1 && old.code === 404 && (await get(u4)).code === 200);
|
||||
|
||||
// delete
|
||||
r = await call(ann, '/api/cal/ncal/delete', { id: fam }); ok('deleting a calendar works', r.status === 200 && (await call(ann, '/api/cal/list')).json.length === 1);
|
||||
r = await call(ann, '/api/cal/ncal/delete', { id: fam }); ok('deleting a calendar works', r.status === 200 && (await call(ann, '/api/cal/list')).json.filter(c => c.id !== 'family~local').length === 1);
|
||||
ok('and removes its events and its share', !(await titles(ann)).includes('Cy adds') && (await titles(cy)) === '');
|
||||
r = await call(ann, '/api/cal/ncal/delete', { id: 'local' }); ok('the last calendar cannot be deleted', r.status === 400, r);
|
||||
ok('older events survived all that', (await titles(ann)).includes('Old event'));
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// The shared Family calendar and Family task list: every person with the tab sees and edits the same ones.
|
||||
const http = require('http'), assert = require('assert'), fs = require('fs'), os = require('os'), path = require('path');
|
||||
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-fam-'));
|
||||
const users = require('../users'), cal = require('../cal'), tasks = require('../tasks');
|
||||
const P = { a: users.create({ name: 'Ann', role: 'admin', handle: 'YQ' }), b: users.create({ name: 'Bob', role: 'user', tabs: ['calendar', 'tasks'], handle: 'Yg' }), c: users.create({ name: 'Cy', role: 'user', tabs: ['notes'], handle: 'Yw' }) };
|
||||
const hub = http.createServer((req, res) => users.runAs(P[req.headers['x-user'] || 'a'], () => req.url.startsWith('/api/tasks') ? tasks.handle(req, res) : cal.handle(req, res))).listen(0, async () => {
|
||||
const B = 'http://127.0.0.1:' + hub.address().port, call = (p, o = {}, who) => fetch(B + p, { ...o, headers: { ...(o.headers || {}), ...(who ? { 'x-user': who } : {}) } }), post = (p, b, who) => call(p, { method: 'POST', body: JSON.stringify(b) }, who), j = r => r.json();
|
||||
try {
|
||||
const day = new Date().toISOString().slice(0, 10), ev = { summary: 'Dinner at Grandma\'s', start: day + 'T18:00', end: day + 'T19:00' };
|
||||
let r = await j(await post('/api/cal/event', { cal: 'family~local', fields: { title: 'Dinner at 6', start: day, end: day, allDay: true } }, 'a')); assert.ok(r.ok, JSON.stringify(r));
|
||||
const win = '/api/cal/events?' + new URLSearchParams({ from: Date.parse(day + 'T00:00:00Z') - 864e5, to: Date.parse(day + 'T00:00:00Z') + 2 * 864e5, fd: day, td: day });
|
||||
let e = await j(await call(win, {}, 'b')); assert.ok(e.events.some(x => x.title === 'Dinner at 6' && x.editable), 'Bob sees and can edit the family event');
|
||||
assert.ok(!(await j(await call(win, {}, 'c'))).events.some(x => x.title === 'Dinner at 6'), 'Cy has no calendar tab: nothing');
|
||||
assert.ok((await j(await call('/api/cal/list', {}, 'b'))).some(c => c.id === 'family~local' && c.name === 'Family'));
|
||||
const lists = (await j(await call('/api/tasks/lists', {}, 'b'))).lists; assert.deepEqual(lists.map(l => l.name), ['Tasks', 'Family']);
|
||||
await post('/api/tasks', { list: 'local:family', fields: { title: 'Book the sitter', due: day } }, 'a');
|
||||
let t = await j(await call('/api/tasks?list=local:family', {}, 'b')); assert.equal(t.length, 1, 'Bob sees the family task'); assert.equal((await j(await call('/api/tasks?list=local:inbox', {}, 'b'))).length, 0);
|
||||
await post('/api/tasks/toggle', { id: t[0].id, list: 'local:family', done: true }, 'b'); t = await j(await call('/api/tasks?list=local:family', {}, 'a')); assert.equal(t[0].done, true, 'ticked by Bob, seen by Ann');
|
||||
const del = await post('/api/tasks/lists/delete', { id: 'local:family' }, 'a'); assert.equal(del.status, 400, 'the Family list stays');
|
||||
console.log('family ok');
|
||||
} catch (e) { console.error(e); process.exitCode = 1; }
|
||||
hub.close(); setTimeout(() => process.exit(process.exitCode || 0), 50);
|
||||
});
|
||||
+1
-1
@@ -6,7 +6,7 @@ const P = { a: users.create({ name: 'Ann', role: 'admin', handle: 'YQ' }), b: us
|
||||
const hub = http.createServer((req, res) => users.runAs(P[req.headers['x-user'] || 'a'], () => notes.handle(req, res))).listen(0, async () => {
|
||||
const B = 'http://127.0.0.1:' + hub.address().port + '/api/notes', call = (p, o = {}, who) => fetch(B + p, { ...o, headers: { ...(o.headers || {}), ...(who ? { 'x-user': who } : {}) } }), post = (p, b, who) => call(p, { method: 'POST', body: JSON.stringify(b) }, who), j = r => r.json(), list = async who => (await j(await call('/', {}, who))).notes;
|
||||
try {
|
||||
const n1 = await j(await post('/', { type: 'note', title: ' Private thoughts ', body: 'secret', color: 'blue' }, 'b')); assert.equal(n1.title, 'Private thoughts'); assert.equal(n1.share, 'me'); assert.equal((await list('a')).length, 0, 'a private note is only for its owner (even an admin does not see it in the list)');
|
||||
const n1 = await j(await post('/', { type: 'note', title: ' Private thoughts ', body: 'secret', color: 'blue', share: 'me' }, 'b')); assert.equal(n1.title, 'Private thoughts'); assert.equal(n1.share, 'me'); assert.equal((await list('a')).length, 0, 'a private note is only for its owner (even an admin does not see it in the list)');
|
||||
assert.equal((await post('/', { id: n1.id, title: 'Peek' }, 'a')).status, 404, 'and cannot be opened');
|
||||
const gro = await j(await post('/', { type: 'list', title: 'Groceries', items: ['Milk', ' ', 'Eggs'], share: 'family' }, 'b')); assert.deepEqual(gro.items.map(x => x.text), ['Milk', 'Eggs'], 'blank items are dropped'); assert.equal((await list('c')).length, 1, 'a family note is seen by everyone with Notes'); assert.equal((await list('d')).length, 0, 'but not by someone without the Notes tab');
|
||||
// checklist: items change one at a time, so two people do not overwrite each other
|
||||
|
||||
Reference in new issue
Block a user