v0.12.21 adjusted mobile group manager ui
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "rosterchirp-backend",
|
||||
"version": "0.12.20",
|
||||
"version": "0.12.21",
|
||||
"description": "RosterChirp backend server",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
|
||||
2
build.sh
2
build.sh
@@ -13,7 +13,7 @@
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:-0.12.20}"
|
||||
VERSION="${1:-0.12.21}"
|
||||
ACTION="${2:-}"
|
||||
REGISTRY="${REGISTRY:-}"
|
||||
IMAGE_NAME="rosterchirp"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "rosterchirp-frontend",
|
||||
"version": "0.12.20",
|
||||
"version": "0.12.21",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -72,6 +72,7 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
const [accordionOpen, setAccordionOpen] = useState(false);
|
||||
|
||||
const load = useCallback(() =>
|
||||
api.getUserGroups().then(({ groups }) => setGroups([...(groups||[])].sort((a, b) => a.name.localeCompare(b.name)))).catch(() => {}), []);
|
||||
@@ -79,6 +80,7 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
|
||||
|
||||
const selectGroup = async (g) => {
|
||||
setShowDelete(false);
|
||||
setAccordionOpen(false);
|
||||
const { members: mems } = await api.getUserGroup(g.id);
|
||||
const ids = new Set(mems.map(m => m.id));
|
||||
setSelected(g); setEditName(g.name); setMembers(ids); setSavedMembers(ids);
|
||||
@@ -141,8 +143,10 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
|
||||
|
||||
return (
|
||||
<div style={{ display:'flex', flexDirection: isMobile ? 'column' : 'row', gap:0, height:'100%', minHeight:0, overflow: isMobile ? 'auto' : 'hidden' }}>
|
||||
{/* Sidebar list */}
|
||||
<div style={{ width: isMobile ? '100%' : 220, flexShrink:0, borderRight: isMobile ? 'none' : '1px solid var(--border)', borderBottom: isMobile ? '1px solid var(--border)' : 'none', overflowY: isMobile ? 'visible' : 'auto', padding:'12px 8px' }}>
|
||||
|
||||
{/* Sidebar — desktop only */}
|
||||
{!isMobile && (
|
||||
<div style={{ width:220, flexShrink:0, borderRight:'1px solid var(--border)', overflowY:'auto', padding:'12px 8px' }}>
|
||||
<div style={{ fontSize:11, fontWeight:700, letterSpacing:'0.8px', textTransform:'uppercase', color:'var(--text-tertiary)', marginBottom:8, paddingLeft:4 }}>User Groups</div>
|
||||
<button onClick={clearSelection} style={{ display:'block', width:'100%', textAlign:'left', padding:'8px 10px', borderRadius:'var(--radius)', border:'none',
|
||||
background:isCreating?'var(--primary-light)':'transparent', color:isCreating?'var(--primary)':'var(--text-secondary)',
|
||||
@@ -159,6 +163,38 @@ function AllGroupsTab({ allUsers, onRefresh, isMobile = false, onIF, onIB }) {
|
||||
))}
|
||||
{groups.length===0 && <div style={{ fontSize:13, color:'var(--text-tertiary)', padding:'8px 4px' }}>No groups yet</div>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile accordion */}
|
||||
{isMobile && (
|
||||
<div style={{ padding:'8px 8px 4px', borderBottom:'1px solid var(--border)', flexShrink:0 }}>
|
||||
<button onClick={clearSelection} style={{ display:'block', width:'100%', textAlign:'left', padding:'8px 10px', borderRadius:'var(--radius)', border:'none',
|
||||
background:isCreating?'var(--primary-light)':'transparent', color:isCreating?'var(--primary)':'var(--text-secondary)',
|
||||
cursor:'pointer', fontWeight:isCreating?600:400, fontSize:13, marginBottom:6 }}>+ New Group</button>
|
||||
<button onClick={() => setAccordionOpen(o => !o)} style={{ display:'flex', alignItems:'center', justifyContent:'space-between', width:'100%', padding:'8px 10px',
|
||||
borderRadius:'var(--radius)', border:'1px solid var(--border)', background:'var(--surface)', cursor:'pointer',
|
||||
fontSize:13, fontWeight:600, color:'var(--text-primary)', marginBottom: accordionOpen ? 4 : 0 }}>
|
||||
<span>Edit Existing</span>
|
||||
<span style={{ fontSize:10, opacity:0.6 }}>{accordionOpen ? '▲' : '▼'}</span>
|
||||
</button>
|
||||
{accordionOpen && (
|
||||
<div style={{ maxHeight:200, overflowY:'auto', border:'1px solid var(--border)', borderRadius:'var(--radius)' }}>
|
||||
{groups.map(g => (
|
||||
<button key={g.id} onClick={() => selectGroup(g)} style={{ display:'block', width:'100%', textAlign:'left', padding:'8px 12px',
|
||||
border:'none', borderBottom:'1px solid var(--border)',
|
||||
background:selected?.id===g.id?'var(--primary-light)':'transparent', color:selected?.id===g.id?'var(--primary)':'var(--text-primary)',
|
||||
cursor:'pointer', fontWeight:selected?.id===g.id?600:400, fontSize:13 }}>
|
||||
<div style={{ display:'flex', alignItems:'center', gap:8 }}>
|
||||
<div style={{ width:22, height:22, borderRadius:5, background:'var(--primary)', display:'flex', alignItems:'center', justifyContent:'center', color:'white', fontSize:8, fontWeight:700, flexShrink:0 }}>UG</div>
|
||||
<div><div style={{ fontSize:13 }}>{g.name}</div><div style={{ fontSize:11, color:'var(--text-tertiary)' }}>{g.member_count} member{g.member_count!==1?'s':''}</div></div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
{groups.length===0 && <div style={{ fontSize:13, color:'var(--text-tertiary)', padding:'8px 12px' }}>No groups yet</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Form */}
|
||||
<div style={{ flex:1, overflowY: isMobile ? 'visible' : 'auto', padding: isMobile ? '16px 12px' : '16px 24px' }}>
|
||||
@@ -238,6 +274,7 @@ function DirectMessagesTab({ allUserGroups, onRefresh, refreshKey, isMobile = fa
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
const [accordionOpen, setAccordionOpen] = useState(false);
|
||||
|
||||
const load = useCallback(() =>
|
||||
api.getMultiGroupDms().then(({ dms }) => setDms([...(dms||[])].sort((a, b) => a.name.localeCompare(b.name)))).catch(() => {}), []);
|
||||
@@ -245,7 +282,7 @@ function DirectMessagesTab({ allUserGroups, onRefresh, refreshKey, isMobile = fa
|
||||
|
||||
const clearSelection = () => { setSelected(null); setDmName(''); setGroupIds(new Set()); setSavedGroupIds(new Set()); setShowDelete(false); };
|
||||
const selectDm = (dm) => {
|
||||
setShowDelete(false); setSelected(dm); setDmName(dm.name);
|
||||
setShowDelete(false); setAccordionOpen(false); setSelected(dm); setDmName(dm.name);
|
||||
const ids = new Set(dm.memberGroupIds||[]); setGroupIds(ids); setSavedGroupIds(ids);
|
||||
};
|
||||
|
||||
@@ -282,7 +319,10 @@ function DirectMessagesTab({ allUserGroups, onRefresh, refreshKey, isMobile = fa
|
||||
|
||||
return (
|
||||
<div style={{ display:'flex', flexDirection: isMobile ? 'column' : 'row', gap:0, height:'100%', minHeight:0, overflow: isMobile ? 'auto' : 'hidden' }}>
|
||||
<div style={{ width: isMobile ? '100%' : 220, flexShrink:0, borderRight: isMobile ? 'none' : '1px solid var(--border)', borderBottom: isMobile ? '1px solid var(--border)' : 'none', overflowY: isMobile ? 'visible' : 'auto', padding:'12px 8px' }}>
|
||||
|
||||
{/* Sidebar — desktop only */}
|
||||
{!isMobile && (
|
||||
<div style={{ width:220, flexShrink:0, borderRight:'1px solid var(--border)', overflowY:'auto', padding:'12px 8px' }}>
|
||||
<div style={{ fontSize:11, fontWeight:700, letterSpacing:'0.8px', textTransform:'uppercase', color:'var(--text-tertiary)', marginBottom:8, paddingLeft:4 }}>Multi-Group DMs</div>
|
||||
<button onClick={clearSelection} style={{ display:'block', width:'100%', textAlign:'left', padding:'8px 10px', borderRadius:'var(--radius)', border:'none',
|
||||
background:isCreating?'var(--primary-light)':'transparent', color:isCreating?'var(--primary)':'var(--text-secondary)',
|
||||
@@ -299,6 +339,39 @@ function DirectMessagesTab({ allUserGroups, onRefresh, refreshKey, isMobile = fa
|
||||
))}
|
||||
{dms.length===0 && <div style={{ fontSize:13, color:'var(--text-tertiary)', padding:'8px 4px' }}>No multi-group DMs yet</div>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile accordion */}
|
||||
{isMobile && (
|
||||
<div style={{ padding:'8px 8px 4px', borderBottom:'1px solid var(--border)', flexShrink:0 }}>
|
||||
<button onClick={clearSelection} style={{ display:'block', width:'100%', textAlign:'left', padding:'8px 10px', borderRadius:'var(--radius)', border:'none',
|
||||
background:isCreating?'var(--primary-light)':'transparent', color:isCreating?'var(--primary)':'var(--text-secondary)',
|
||||
cursor:'pointer', fontWeight:isCreating?600:400, fontSize:13, marginBottom:6 }}>+ New Multi-Group DM</button>
|
||||
<button onClick={() => setAccordionOpen(o => !o)} style={{ display:'flex', alignItems:'center', justifyContent:'space-between', width:'100%', padding:'8px 10px',
|
||||
borderRadius:'var(--radius)', border:'1px solid var(--border)', background:'var(--surface)', cursor:'pointer',
|
||||
fontSize:13, fontWeight:600, color:'var(--text-primary)', marginBottom: accordionOpen ? 4 : 0 }}>
|
||||
<span>Edit Existing</span>
|
||||
<span style={{ fontSize:10, opacity:0.6 }}>{accordionOpen ? '▲' : '▼'}</span>
|
||||
</button>
|
||||
{accordionOpen && (
|
||||
<div style={{ maxHeight:200, overflowY:'auto', border:'1px solid var(--border)', borderRadius:'var(--radius)' }}>
|
||||
{dms.map(dm => (
|
||||
<button key={dm.id} onClick={() => selectDm(dm)} style={{ display:'block', width:'100%', textAlign:'left', padding:'8px 12px',
|
||||
border:'none', borderBottom:'1px solid var(--border)',
|
||||
background:selected?.id===dm.id?'var(--primary-light)':'transparent', color:selected?.id===dm.id?'var(--primary)':'var(--text-primary)',
|
||||
cursor:'pointer', fontWeight:selected?.id===dm.id?600:400, fontSize:13 }}>
|
||||
<div style={{ display:'flex', alignItems:'center', gap:8 }}>
|
||||
<div style={{ width:22, height:22, borderRadius:5, background:'var(--primary)', display:'flex', alignItems:'center', justifyContent:'center', color:'white', fontSize:8, fontWeight:700, flexShrink:0 }}>MG</div>
|
||||
<div><div style={{ fontSize:13 }}>{dm.name}</div><div style={{ fontSize:11, color:'var(--text-tertiary)' }}>{dm.group_count} group{dm.group_count!==1?'s':''}</div></div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
{dms.length===0 && <div style={{ fontSize:13, color:'var(--text-tertiary)', padding:'8px 12px' }}>No multi-group DMs yet</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ flex:1, overflowY: isMobile ? 'visible' : 'auto', padding: isMobile ? '16px 12px' : '16px 24px' }}>
|
||||
<div style={{ display:'flex', flexDirection:'column', gap:18, maxWidth: isMobile ? '100%' : 520 }}>
|
||||
<div>
|
||||
@@ -345,6 +418,7 @@ function U2URestrictionsTab({ allUserGroups, isMobile = false, onIF, onIB }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [accordionOpen, setAccordionOpen] = useState(true);
|
||||
// Map of groupId → number of restrictions (for showing dots in sidebar)
|
||||
const [restrictionCounts, setRestrictionCounts] = useState({});
|
||||
|
||||
@@ -376,6 +450,7 @@ function U2URestrictionsTab({ allUserGroups, isMobile = false, onIF, onIB }) {
|
||||
const selectGroup = (g) => {
|
||||
setSelectedGroup(g);
|
||||
setSearch('');
|
||||
setAccordionOpen(false);
|
||||
loadRestrictions(g);
|
||||
};
|
||||
|
||||
@@ -413,14 +488,7 @@ function U2URestrictionsTab({ allUserGroups, isMobile = false, onIF, onIB }) {
|
||||
? otherGroups.filter(g => g.name.toLowerCase().includes(search.toLowerCase()))
|
||||
: otherGroups;
|
||||
|
||||
return (
|
||||
<div style={{ display:'flex', flexDirection: isMobile ? 'column' : 'row', gap:0, height:'100%', minHeight:0, overflow: isMobile ? 'auto' : 'hidden' }}>
|
||||
{/* Group selector sidebar */}
|
||||
<div style={{ width: isMobile ? '100%' : 220, flexShrink:0, borderRight: isMobile ? 'none' : '1px solid var(--border)', borderBottom: isMobile ? '1px solid var(--border)' : 'none', overflowY: isMobile ? 'visible' : 'auto', padding:'12px 8px' }}>
|
||||
<div style={{ fontSize:11, fontWeight:700, letterSpacing:'0.8px', textTransform:'uppercase', color:'var(--text-tertiary)', marginBottom:8, paddingLeft:4 }}>
|
||||
Select Group
|
||||
</div>
|
||||
{allUserGroups.map(g => {
|
||||
const u2uGroupButton = (g) => {
|
||||
const hasRestrictions = g.id === selectedGroup?.id ? blockedIds.size > 0 : (restrictionCounts[g.id] || 0) > 0;
|
||||
return (
|
||||
<button key={g.id} onClick={() => selectGroup(g)} style={{
|
||||
@@ -442,11 +510,58 @@ function U2URestrictionsTab({ allUserGroups, isMobile = false, onIF, onIB }) {
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display:'flex', flexDirection: isMobile ? 'column' : 'row', gap:0, height:'100%', minHeight:0, overflow: isMobile ? 'auto' : 'hidden' }}>
|
||||
|
||||
{/* Group selector — desktop sidebar */}
|
||||
{!isMobile && (
|
||||
<div style={{ width:220, flexShrink:0, borderRight:'1px solid var(--border)', overflowY:'auto', padding:'12px 8px' }}>
|
||||
<div style={{ fontSize:11, fontWeight:700, letterSpacing:'0.8px', textTransform:'uppercase', color:'var(--text-tertiary)', marginBottom:8, paddingLeft:4 }}>
|
||||
Select Group
|
||||
</div>
|
||||
{allUserGroups.map(g => u2uGroupButton(g))}
|
||||
{allUserGroups.length === 0 && (
|
||||
<div style={{ fontSize:13, color:'var(--text-tertiary)', padding:'8px 4px' }}>No user groups yet</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile accordion — expanded by default, collapses on selection */}
|
||||
{isMobile && (
|
||||
<div style={{ padding:'8px 8px 4px', borderBottom:'1px solid var(--border)', flexShrink:0 }}>
|
||||
<button onClick={() => setAccordionOpen(o => !o)} style={{ display:'flex', alignItems:'center', justifyContent:'space-between', width:'100%', padding:'8px 10px',
|
||||
borderRadius:'var(--radius)', border:'1px solid var(--border)', background:'var(--surface)', cursor:'pointer',
|
||||
fontSize:13, fontWeight:600, color:'var(--text-primary)', marginBottom: accordionOpen ? 4 : 0 }}>
|
||||
<span>Select Group</span>
|
||||
<span style={{ fontSize:10, opacity:0.6 }}>{accordionOpen ? '▲' : '▼'}</span>
|
||||
</button>
|
||||
{accordionOpen && (
|
||||
<div style={{ maxHeight:200, overflowY:'auto', border:'1px solid var(--border)', borderRadius:'var(--radius)' }}>
|
||||
{allUserGroups.map(g => {
|
||||
const hasRestrictions = g.id === selectedGroup?.id ? blockedIds.size > 0 : (restrictionCounts[g.id] || 0) > 0;
|
||||
return (
|
||||
<button key={g.id} onClick={() => selectGroup(g)} style={{ display:'block', width:'100%', textAlign:'left', padding:'8px 12px',
|
||||
border:'none', borderBottom:'1px solid var(--border)',
|
||||
background:selectedGroup?.id===g.id?'var(--primary-light)':'transparent', color:selectedGroup?.id===g.id?'var(--primary)':'var(--text-primary)',
|
||||
cursor:'pointer', fontWeight:selectedGroup?.id===g.id?600:400, fontSize:13 }}>
|
||||
<div style={{ display:'flex', alignItems:'center', gap:8 }}>
|
||||
<div style={{ width:22, height:22, borderRadius:5, background:'var(--primary)', display:'flex', alignItems:'center', justifyContent:'center', color:'white', fontSize:8, fontWeight:700, flexShrink:0 }}>UG</div>
|
||||
<div style={{ flex:1, minWidth:0 }}>
|
||||
<div style={{ fontSize:13, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{g.name}</div>
|
||||
<div style={{ fontSize:11, color:'var(--text-tertiary)' }}>{g.member_count} member{g.member_count!==1?'s':''}</div>
|
||||
</div>
|
||||
{hasRestrictions && <span style={{ width:8, height:8, borderRadius:'50%', background:'var(--error)', flexShrink:0 }} />}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{allUserGroups.length === 0 && <div style={{ fontSize:13, color:'var(--text-tertiary)', padding:'8px 12px' }}>No user groups yet</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Restriction editor */}
|
||||
<div style={{ flex:1, overflowY: isMobile ? 'visible' : 'auto', padding: isMobile ? '16px 12px' : '16px 24px' }}>
|
||||
|
||||
Reference in New Issue
Block a user