diff --git a/backend/package.json b/backend/package.json index c163796..8c9f05f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-backend", - "version": "0.12.19", + "version": "0.12.20", "description": "RosterChirp backend server", "main": "src/index.js", "scripts": { diff --git a/backend/src/routes/usergroups.js b/backend/src/routes/usergroups.js index de49512..e0b6471 100644 --- a/backend/src/routes/usergroups.js +++ b/backend/src/routes/usergroups.js @@ -209,17 +209,21 @@ router.get('/:id', authMiddleware, teamManagerMiddleware, async (req, res) => { // POST / — create user group router.post('/', authMiddleware, teamManagerMiddleware, async (req, res) => { - const { name, memberIds = [] } = req.body; + const { name, memberIds = [], noDm = false } = req.body; if (!name?.trim()) return res.status(400).json({ error: 'Name required' }); try { const existing = await queryOne(req.schema, 'SELECT id FROM user_groups WHERE LOWER(name)=LOWER($1)', [name.trim()]); if (existing) return res.status(400).json({ error: 'Name already in use' }); - // Create the managed DM group - const gr = await queryResult(req.schema, - "INSERT INTO groups (name,type,is_readonly,is_managed) VALUES ($1,'private',FALSE,TRUE) RETURNING id", - [name.trim()] - ); - const dmGroupId = gr.rows[0].id; + + let dmGroupId = null; + if (!noDm) { + const gr = await queryResult(req.schema, + "INSERT INTO groups (name,type,is_readonly,is_managed) VALUES ($1,'private',FALSE,TRUE) RETURNING id", + [name.trim()] + ); + dmGroupId = gr.rows[0].id; + } + const ugr = await queryResult(req.schema, 'INSERT INTO user_groups (name,dm_group_id) VALUES ($1,$2) RETURNING id', [name.trim(), dmGroupId] @@ -229,7 +233,7 @@ router.post('/', authMiddleware, teamManagerMiddleware, async (req, res) => { for (const uid of memberIds) { if (defaultAdmin && uid === defaultAdmin.id) continue; await exec(req.schema, 'INSERT INTO user_group_members (user_group_id,user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [ugId, uid]); - await addUserSilent(req.schema, dmGroupId, uid); + if (dmGroupId) await addUserSilent(req.schema, dmGroupId, uid); } const ug = await queryOne(req.schema, 'SELECT * FROM user_groups WHERE id=$1', [ugId]); res.json({ userGroup: ug }); @@ -238,9 +242,9 @@ router.post('/', authMiddleware, teamManagerMiddleware, async (req, res) => { // PATCH /:id router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) => { - const { name, memberIds } = req.body; + const { name, memberIds, createDm = false } = req.body; try { - const ug = await queryOne(req.schema, 'SELECT * FROM user_groups WHERE id=$1', [req.params.id]); + let ug = await queryOne(req.schema, 'SELECT * FROM user_groups WHERE id=$1', [req.params.id]); if (!ug) return res.status(404).json({ error: 'Not found' }); if (name && name.trim() !== ug.name) { @@ -250,6 +254,23 @@ router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) => if (ug.dm_group_id) await exec(req.schema, 'UPDATE groups SET name=$1, updated_at=NOW() WHERE id=$2', [name.trim(), ug.dm_group_id]); } + // Create DM group if requested and one doesn't exist yet + if (createDm && !ug.dm_group_id) { + const groupName = (name?.trim()) || ug.name; + const gr = await queryResult(req.schema, + "INSERT INTO groups (name,type,is_readonly,is_managed) VALUES ($1,'private',FALSE,TRUE) RETURNING id", + [groupName] + ); + const newDmId = gr.rows[0].id; + await exec(req.schema, 'UPDATE user_groups SET dm_group_id=$1, updated_at=NOW() WHERE id=$2', [newDmId, ug.id]); + ug = await queryOne(req.schema, 'SELECT * FROM user_groups WHERE id=$1', [ug.id]); + // Add all current members to the new DM silently (no per-user join messages for a bulk creation) + const currentMembers = await query(req.schema, 'SELECT user_id FROM user_group_members WHERE user_group_id=$1', [ug.id]); + for (const { user_id } of currentMembers) { + await addUserSilent(req.schema, newDmId, user_id); + } + } + if (Array.isArray(memberIds) && ug.dm_group_id) { const defaultAdmin = await queryOne(req.schema, 'SELECT id FROM users WHERE is_default_admin=TRUE'); const newIds = new Set(memberIds.map(Number).filter(Boolean)); diff --git a/build.sh b/build.sh index f5e5086..8526633 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.12.19}" +VERSION="${1:-0.12.20}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="rosterchirp" diff --git a/frontend/package.json b/frontend/package.json index 3643fea..cad5604 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-frontend", - "version": "0.12.19", + "version": "0.12.20", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/pages/GroupManagerPage.jsx b/frontend/src/pages/GroupManagerPage.jsx index 4c59e6a..20c1eb8 100644 --- a/frontend/src/pages/GroupManagerPage.jsx +++ b/frontend/src/pages/GroupManagerPage.jsx @@ -68,6 +68,7 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) { const [members, setMembers] = useState(new Set()); const [fullMembers, setFullMembers] = useState([]); // full member objects including deleted const [editName, setEditName] = useState(''); + const [noDm, setNoDm] = useState(false); const [saving, setSaving] = useState(false); const [deleting, setDeleting] = useState(false); const [showDelete, setShowDelete] = useState(false); @@ -82,10 +83,12 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) { const ids = new Set(mems.map(m => m.id)); setSelected(g); setEditName(g.name); setMembers(ids); setSavedMembers(ids); setFullMembers(mems); + // No DM → checkbox enabled+checked; has DM → checkbox disabled+unchecked + setNoDm(!g.dm_group_id); }; const clearSelection = () => { setSelected(null); setEditName(''); setMembers(new Set()); setSavedMembers(new Set()); - setShowDelete(false); setFullMembers([]); + setShowDelete(false); setFullMembers([]); setNoDm(false); }; const handleSave = async () => { @@ -93,14 +96,18 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) { setSaving(true); try { if (selected) { - await api.updateUserGroup(selected.id, { name: editName.trim(), memberIds: [...members] }); + // createDm=true when the group has no DM and the user unchecked "Do not create Group DM" + const createDm = !selected.dm_group_id && !noDm; + const { group: updated } = await api.updateUserGroup(selected.id, { name: editName.trim(), memberIds: [...members], createDm }); toast('Group updated', 'success'); const { members: fresh } = await api.getUserGroup(selected.id); const freshIds = new Set(fresh.map(m => m.id)); setSavedMembers(freshIds); setMembers(freshIds); setFullMembers(fresh); - setSelected(prev => ({ ...prev, name: editName.trim() })); + // Reflect new dm_group_id if a DM was just created + setSelected(prev => ({ ...prev, name: editName.trim(), dm_group_id: updated?.dm_group_id ?? prev.dm_group_id })); + if (createDm) setNoDm(false); } else { - await api.createUserGroup({ name: editName.trim(), memberIds: [...members] }); + await api.createUserGroup({ name: editName.trim(), memberIds: [...members], noDm }); toast(`Group "${editName.trim()}" created`, 'success'); clearSelection(); } @@ -159,7 +166,18 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
A matching Direct Message group will be created automatically.
} + {isCreating && !noDm &&A matching Direct Message group will be created automatically.
} + + {selected && selected.dm_group_id &&Group DM already exists — cannot be removed.
}