feat(activities): let a group show and limit its members

Empty membership stays open to the whole workspace; picking people restricts who can see that activity group.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-28 20:24:40 +03:30
co-authored by Cursor
parent 2cc2d358e4
commit 63a1803adc
8 changed files with 272 additions and 22 deletions
+128 -5
View File
@@ -170,7 +170,8 @@ const board = computed<string>(() => {
});
const currentBoard = computed(() => boards.value.find((b) => b.id === board.value) ?? null);
const boardMenuOpen = ref(false);
const boardModal = ref<{ mode: 'create' | 'edit'; name: string } | null>(null);
const boardModal = ref<{ mode: 'create' | 'edit'; name: string; memberIds: string[] } | null>(null);
const boardMemberQuery = ref('');
const boardSaving = ref(false);
const boardDeleteOpen = ref(false);
@@ -410,13 +411,76 @@ function setBoard(next: string) {
});
}
const activeBoardUsers = computed(() => users.value.filter((user) => user.isActive !== false));
const boardMembersOpenToAll = computed(() => {
const ids = activeBoardUsers.value.map((user) => user.id);
const selected = boardModal.value?.memberIds ?? [];
return ids.length > 0 && ids.every((id) => selected.includes(id));
});
const filteredBoardUsers = computed(() => {
const query = boardMemberQuery.value.trim().replace(/ي/g, 'ی').replace(/ك/g, 'ک');
const selected = new Set(boardModal.value?.memberIds ?? []);
const list = activeBoardUsers.value.filter((user) => {
if (!query) return true;
return user.fullName.replace(/ي/g, 'ی').replace(/ك/g, 'ک').includes(query);
});
return list.slice().sort((a, b) => Number(selected.has(b.id)) - Number(selected.has(a.id)));
});
function showExistingBoardMembers() {
const modal = boardModal.value;
if (!modal || modal.mode !== 'edit') return;
const saved = currentBoard.value?.memberIds ?? [];
modal.memberIds = saved.length
? [...saved]
: activeBoardUsers.value.map((user) => user.id);
}
async function ensureBoardUsers() {
if (!users.value.length) {
try {
const { data: rows } = await api.get<OrgUserDto[]>('/users');
users.value = rows.filter((user) => user.isActive !== false);
} catch {
users.value = [];
}
}
showExistingBoardMembers();
}
function memberIdsToSave(selected: string[]) {
const all = activeBoardUsers.value.map((user) => user.id);
if (all.length && selected.length === all.length && all.every((id) => selected.includes(id))) {
return [];
}
return selected;
}
function openCreateBoard() {
boardModal.value = { mode: 'create', name: '' };
boardMemberQuery.value = '';
boardModal.value = { mode: 'create', name: '', memberIds: [] };
void ensureBoardUsers();
}
function openEditBoard() {
if (!currentBoard.value) return;
boardModal.value = { mode: 'edit', name: currentBoard.value.name };
boardMemberQuery.value = '';
boardModal.value = {
mode: 'edit',
name: currentBoard.value.name,
memberIds: [...(currentBoard.value.memberIds ?? [])],
};
void ensureBoardUsers();
}
function toggleBoardMember(userId: string) {
const modal = boardModal.value;
if (!modal) return;
modal.memberIds = modal.memberIds.includes(userId)
? modal.memberIds.filter((id) => id !== userId)
: [...modal.memberIds, userId];
}
async function saveBoard() {
@@ -427,12 +491,18 @@ async function saveBoard() {
error.value = '';
try {
if (modal.mode === 'create') {
const { data: created } = await api.post<ActivityBoardDto>('/activities/boards', { name });
const { data: created } = await api.post<ActivityBoardDto>('/activities/boards', {
name,
memberIds: memberIdsToSave(modal.memberIds),
});
await loadBoards();
boardModal.value = null;
setBoard(created.id);
} else if (currentBoard.value) {
await api.patch(`/activities/boards/${currentBoard.value.id}`, { name });
await api.patch(`/activities/boards/${currentBoard.value.id}`, {
name,
memberIds: memberIdsToSave(modal.memberIds),
});
await loadBoards();
boardModal.value = null;
}
@@ -1620,6 +1690,34 @@ onUnmounted(() => {
<p v-if="boardModal.mode === 'create'" class="board-form-hint">
گروه جدید با ستون‌های پیش‌فرض ساخته می‌شود و بعد می‌توانید ستون‌هایش را تغییر دهید.
</p>
<label class="board-form-label" for="activity-board-member-search">اعضا</label>
<UiInput
id="activity-board-member-search"
v-model="boardMemberQuery"
placeholder="جستجوی نام عضو…"
/>
<p class="board-form-hint">
{{
boardMembersOpenToAll
? `اعضای فعلی همین ${toFaDigits(boardModal.memberIds.length)} نفر هستند و گروه برای همهٔ محیط باز است. با برداشتن تیک، فقط افراد انتخاب‌شده آن را می‌بینند.`
: boardModal.memberIds.length
? `${toFaDigits(boardModal.memberIds.length)} نفر انتخاب شده. فقط همین افراد این گروه را می‌بینند.`
: 'اگر کسی را انتخاب نکنید، این گروه برای همهٔ اعضای محیط دیده می‌شود.'
}}
</p>
<ul class="board-members">
<li v-if="!filteredBoardUsers.length" class="board-form-hint">عضوی پیدا نشد.</li>
<li v-for="user in filteredBoardUsers" :key="user.id">
<label>
<input
type="checkbox"
:checked="boardModal.memberIds.includes(user.id)"
@change="toggleBoardMember(user.id)"
/>
<span>{{ user.fullName }}</span>
</label>
</li>
</ul>
<UiButton
v-if="boardModal.mode === 'edit' && currentBoard && !currentBoard.isDefault"
variant="ghost"
@@ -1890,6 +1988,31 @@ h1 {
line-height: 1.6;
}
.board-members {
list-style: none;
margin: 0;
padding: 0.25rem;
max-height: 14rem;
overflow: auto;
border: 1px solid #e2e8f0;
border-radius: var(--radius);
display: grid;
gap: 0.1rem;
}
.board-members label {
display: flex;
align-items: center;
gap: 0.45rem;
padding: 0.35rem 0.45rem;
border-radius: var(--radius);
cursor: pointer;
}
.board-members label:hover {
background: #f8fafc;
}
.board-delete {
justify-self: start;
color: var(--danger);