v0.12.20 added option to not create user group dm
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rosterchirp-backend",
|
"name": "rosterchirp-backend",
|
||||||
"version": "0.12.19",
|
"version": "0.12.20",
|
||||||
"description": "RosterChirp backend server",
|
"description": "RosterChirp backend server",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -209,17 +209,21 @@ router.get('/:id', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
|||||||
|
|
||||||
// POST / — create user group
|
// POST / — create user group
|
||||||
router.post('/', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
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' });
|
if (!name?.trim()) return res.status(400).json({ error: 'Name required' });
|
||||||
try {
|
try {
|
||||||
const existing = await queryOne(req.schema, 'SELECT id FROM user_groups WHERE LOWER(name)=LOWER($1)', [name.trim()]);
|
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' });
|
if (existing) return res.status(400).json({ error: 'Name already in use' });
|
||||||
// Create the managed DM group
|
|
||||||
const gr = await queryResult(req.schema,
|
let dmGroupId = null;
|
||||||
"INSERT INTO groups (name,type,is_readonly,is_managed) VALUES ($1,'private',FALSE,TRUE) RETURNING id",
|
if (!noDm) {
|
||||||
[name.trim()]
|
const gr = await queryResult(req.schema,
|
||||||
);
|
"INSERT INTO groups (name,type,is_readonly,is_managed) VALUES ($1,'private',FALSE,TRUE) RETURNING id",
|
||||||
const dmGroupId = gr.rows[0].id;
|
[name.trim()]
|
||||||
|
);
|
||||||
|
dmGroupId = gr.rows[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
const ugr = await queryResult(req.schema,
|
const ugr = await queryResult(req.schema,
|
||||||
'INSERT INTO user_groups (name,dm_group_id) VALUES ($1,$2) RETURNING id',
|
'INSERT INTO user_groups (name,dm_group_id) VALUES ($1,$2) RETURNING id',
|
||||||
[name.trim(), dmGroupId]
|
[name.trim(), dmGroupId]
|
||||||
@@ -229,7 +233,7 @@ router.post('/', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
|||||||
for (const uid of memberIds) {
|
for (const uid of memberIds) {
|
||||||
if (defaultAdmin && uid === defaultAdmin.id) continue;
|
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 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]);
|
const ug = await queryOne(req.schema, 'SELECT * FROM user_groups WHERE id=$1', [ugId]);
|
||||||
res.json({ userGroup: ug });
|
res.json({ userGroup: ug });
|
||||||
@@ -238,9 +242,9 @@ router.post('/', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
|||||||
|
|
||||||
// PATCH /:id
|
// PATCH /:id
|
||||||
router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
router.patch('/:id', authMiddleware, teamManagerMiddleware, async (req, res) => {
|
||||||
const { name, memberIds } = req.body;
|
const { name, memberIds, createDm = false } = req.body;
|
||||||
try {
|
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 (!ug) return res.status(404).json({ error: 'Not found' });
|
||||||
|
|
||||||
if (name && name.trim() !== ug.name) {
|
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]);
|
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) {
|
if (Array.isArray(memberIds) && ug.dm_group_id) {
|
||||||
const defaultAdmin = await queryOne(req.schema, 'SELECT id FROM users WHERE is_default_admin=TRUE');
|
const defaultAdmin = await queryOne(req.schema, 'SELECT id FROM users WHERE is_default_admin=TRUE');
|
||||||
const newIds = new Set(memberIds.map(Number).filter(Boolean));
|
const newIds = new Set(memberIds.map(Number).filter(Boolean));
|
||||||
|
|||||||
2
build.sh
2
build.sh
@@ -13,7 +13,7 @@
|
|||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="${1:-0.12.19}"
|
VERSION="${1:-0.12.20}"
|
||||||
ACTION="${2:-}"
|
ACTION="${2:-}"
|
||||||
REGISTRY="${REGISTRY:-}"
|
REGISTRY="${REGISTRY:-}"
|
||||||
IMAGE_NAME="rosterchirp"
|
IMAGE_NAME="rosterchirp"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rosterchirp-frontend",
|
"name": "rosterchirp-frontend",
|
||||||
"version": "0.12.19",
|
"version": "0.12.20",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
|
|||||||
const [members, setMembers] = useState(new Set());
|
const [members, setMembers] = useState(new Set());
|
||||||
const [fullMembers, setFullMembers] = useState([]); // full member objects including deleted
|
const [fullMembers, setFullMembers] = useState([]); // full member objects including deleted
|
||||||
const [editName, setEditName] = useState('');
|
const [editName, setEditName] = useState('');
|
||||||
|
const [noDm, setNoDm] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const [showDelete, setShowDelete] = 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));
|
const ids = new Set(mems.map(m => m.id));
|
||||||
setSelected(g); setEditName(g.name); setMembers(ids); setSavedMembers(ids);
|
setSelected(g); setEditName(g.name); setMembers(ids); setSavedMembers(ids);
|
||||||
setFullMembers(mems);
|
setFullMembers(mems);
|
||||||
|
// No DM → checkbox enabled+checked; has DM → checkbox disabled+unchecked
|
||||||
|
setNoDm(!g.dm_group_id);
|
||||||
};
|
};
|
||||||
const clearSelection = () => {
|
const clearSelection = () => {
|
||||||
setSelected(null); setEditName(''); setMembers(new Set()); setSavedMembers(new Set());
|
setSelected(null); setEditName(''); setMembers(new Set()); setSavedMembers(new Set());
|
||||||
setShowDelete(false); setFullMembers([]);
|
setShowDelete(false); setFullMembers([]); setNoDm(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
@@ -93,14 +96,18 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
|
|||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
if (selected) {
|
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');
|
toast('Group updated', 'success');
|
||||||
const { members: fresh } = await api.getUserGroup(selected.id);
|
const { members: fresh } = await api.getUserGroup(selected.id);
|
||||||
const freshIds = new Set(fresh.map(m => m.id));
|
const freshIds = new Set(fresh.map(m => m.id));
|
||||||
setSavedMembers(freshIds); setMembers(freshIds); setFullMembers(fresh);
|
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 {
|
} else {
|
||||||
await api.createUserGroup({ name: editName.trim(), memberIds: [...members] });
|
await api.createUserGroup({ name: editName.trim(), memberIds: [...members], noDm });
|
||||||
toast(`Group "${editName.trim()}" created`, 'success');
|
toast(`Group "${editName.trim()}" created`, 'success');
|
||||||
clearSelection();
|
clearSelection();
|
||||||
}
|
}
|
||||||
@@ -159,7 +166,18 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
|
|||||||
<div>
|
<div>
|
||||||
<label className="settings-section-label">Group Name</label>
|
<label className="settings-section-label">Group Name</label>
|
||||||
<input className="input" value={editName} onChange={e => setEditName(e.target.value)} autoComplete="new-password" placeholder="e.g. Coaches" style={{ marginTop:6 }} autoComplete="new-password" onFocus={onIF} onBlur={onIB} />
|
<input className="input" value={editName} onChange={e => setEditName(e.target.value)} autoComplete="new-password" placeholder="e.g. Coaches" style={{ marginTop:6 }} autoComplete="new-password" onFocus={onIF} onBlur={onIB} />
|
||||||
{isCreating && <p style={{ fontSize:12, color:'var(--text-tertiary)', marginTop:5 }}>A matching Direct Message group will be created automatically.</p>}
|
{isCreating && !noDm && <p style={{ fontSize:12, color:'var(--text-tertiary)', marginTop:5 }}>A matching Direct Message group will be created automatically.</p>}
|
||||||
|
<label style={{ display:'flex', alignItems:'center', gap:8, marginTop:8, cursor: (selected && selected.dm_group_id) ? 'not-allowed' : 'pointer', opacity: (selected && selected.dm_group_id) ? 0.5 : 1 }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={noDm}
|
||||||
|
disabled={!!(selected && selected.dm_group_id)}
|
||||||
|
onChange={e => setNoDm(e.target.checked)}
|
||||||
|
style={{ width:15, height:15, cursor: (selected && selected.dm_group_id) ? 'not-allowed' : 'pointer' }}
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize:13, color:'var(--text-secondary)' }}>Do not create Group DM</span>
|
||||||
|
</label>
|
||||||
|
{selected && selected.dm_group_id && <p style={{ fontSize:12, color:'var(--text-tertiary)', marginTop:4 }}>Group DM already exists — cannot be removed.</p>}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="settings-section-label">Members</label>
|
<label className="settings-section-label">Members</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user