feat(activities): let creators lock edit and delete to themselves
Add editPolicy (default creator-only), enforce it in the API, surface clear permission messages, and drop the redundant edit menu now that detail is editable. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -52,7 +52,10 @@ import type {
|
||||
} from '@mento/shared';
|
||||
import { brand } from '@mento/shared';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { canMutateActivity } from '@/lib/activity-access';
|
||||
import { canManageSettings, canViewAllRecords } from '@/lib/roles';
|
||||
import { apiError } from '@/lib/routines';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { PAGE_SIZE_OPTIONS, usePageSize } from '@/lib/pageSize';
|
||||
import { registerTableColumns, useTableColumns } from '@/lib/tableColumns';
|
||||
import TableColumnsMenu from '@/components/TableColumnsMenu.vue';
|
||||
@@ -446,23 +449,11 @@ function deskDetailQuery(columnStageId?: string): Record<string, string> {
|
||||
return query;
|
||||
}
|
||||
|
||||
function activityRoute(id: string, edit = false, columnStageId?: string) {
|
||||
if (!edit) {
|
||||
return {
|
||||
name: 'activity-detail' as const,
|
||||
params: { id },
|
||||
query: deskDetailQuery(columnStageId),
|
||||
};
|
||||
}
|
||||
const query: Record<string, string> = {};
|
||||
for (const key of ['board', 'view', 'desk', 'filter', 'stage', 'q', 'typeDefId', 'status', 'assigneeId', 'title', 'customer', 'due'] as const) {
|
||||
const value = route.query[key];
|
||||
if (typeof value === 'string' && value) query[key] = value;
|
||||
}
|
||||
function activityRoute(id: string, columnStageId?: string) {
|
||||
return {
|
||||
name: 'activity-edit' as const,
|
||||
name: 'activity-detail' as const,
|
||||
params: { id },
|
||||
query,
|
||||
query: deskDetailQuery(columnStageId),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -938,10 +929,27 @@ function clearSelect() {
|
||||
selectedIds.value = new Set();
|
||||
}
|
||||
|
||||
function canDeleteActivity(item: ActivityDto | null | undefined) {
|
||||
return canMutateActivity(item, auth.user);
|
||||
}
|
||||
|
||||
function activityById(id: string) {
|
||||
return (
|
||||
items.value.find((row) => row.id === id) ||
|
||||
(boardMenuActivity.value?.id === id ? boardMenuActivity.value : null)
|
||||
);
|
||||
}
|
||||
|
||||
function askDeleteActivity(id: string, title?: string) {
|
||||
menuId.value = null;
|
||||
boardMenuId.value = null;
|
||||
const target = activityById(id);
|
||||
boardMenuActivity.value = null;
|
||||
if (target && !canDeleteActivity(target)) {
|
||||
message.warning('شما مجوز حذف این فعالیت را ندارید.');
|
||||
error.value = 'شما مجوز حذف این فعالیت را ندارید.';
|
||||
return;
|
||||
}
|
||||
deleteSingleTitle.value = title?.trim() || activityTitle(id);
|
||||
deleteActivityIds.value = [id];
|
||||
}
|
||||
@@ -951,8 +959,24 @@ function askBulkDelete() {
|
||||
menuId.value = null;
|
||||
boardMenuId.value = null;
|
||||
boardMenuActivity.value = null;
|
||||
const selected = [...selectedIds.value];
|
||||
const allowed = selected.filter((id) => {
|
||||
const row = activityById(id);
|
||||
return !row || canDeleteActivity(row);
|
||||
});
|
||||
const blocked = selected.length - allowed.length;
|
||||
if (!allowed.length) {
|
||||
message.warning('شما مجوز حذف این فعالیتها را ندارید.');
|
||||
error.value = 'شما مجوز حذف این فعالیتها را ندارید.';
|
||||
return;
|
||||
}
|
||||
if (blocked > 0) {
|
||||
message.warning(
|
||||
`${toFaDigits(blocked)} مورد بهخاطر نبود مجوز حذف کنار گذاشته شد.`,
|
||||
);
|
||||
}
|
||||
deleteSingleTitle.value = '';
|
||||
deleteActivityIds.value = [...selectedIds.value];
|
||||
deleteActivityIds.value = allowed;
|
||||
}
|
||||
|
||||
async function confirmDeleteActivities() {
|
||||
@@ -962,16 +986,24 @@ async function confirmDeleteActivities() {
|
||||
error.value = '';
|
||||
try {
|
||||
const results = await Promise.allSettled(ids.map((id) => api.delete(`/activities/${id}`)));
|
||||
const failed = results.filter((r) => r.status === 'rejected').length;
|
||||
const failed = results.filter((r) => r.status === 'rejected');
|
||||
deleteActivityIds.value = [];
|
||||
clearSelect();
|
||||
kanbanReload.value += 1;
|
||||
await refreshAll();
|
||||
if (failed) {
|
||||
if (failed.length) {
|
||||
const first = failed[0];
|
||||
const detail =
|
||||
first && first.status === 'rejected'
|
||||
? apiError(first.reason, '')
|
||||
: '';
|
||||
error.value =
|
||||
failed === ids.length
|
||||
? 'حذف فعالیتها ناموفق بود.'
|
||||
: `${toFaDigits(failed)} مورد حذف نشد.`;
|
||||
failed.length === ids.length
|
||||
? detail || 'شما مجوز حذف این فعالیت را ندارید.'
|
||||
: detail
|
||||
? `${toFaDigits(failed.length)} مورد حذف نشد: ${detail}`
|
||||
: `${toFaDigits(failed.length)} مورد حذف نشد.`;
|
||||
message.error(error.value);
|
||||
}
|
||||
} finally {
|
||||
activityDeleting.value = false;
|
||||
@@ -1776,16 +1808,18 @@ onUnmounted(() => {
|
||||
<RouterLink :to="activityRoute(item.id)">
|
||||
مشاهده
|
||||
</RouterLink>
|
||||
<RouterLink :to="activityRoute(item.id, true)">
|
||||
ویرایش
|
||||
</RouterLink>
|
||||
<button v-if="canPromotePrior(item)" type="button" @click="promotePriorActivity(item.id)">
|
||||
انتقال به کار جاری
|
||||
</button>
|
||||
<button v-if="canMarkShare(item)" type="button" @click="complete(item.id)">
|
||||
{{ completeLabel(item) }}
|
||||
</button>
|
||||
<button type="button" class="danger" @click="askDeleteActivity(item.id, item.title)">
|
||||
<button
|
||||
v-if="canDeleteActivity(item)"
|
||||
type="button"
|
||||
class="danger"
|
||||
@click="askDeleteActivity(item.id, item.title)"
|
||||
>
|
||||
حذف
|
||||
</button>
|
||||
</UiDropdownMenu>
|
||||
@@ -1866,16 +1900,18 @@ onUnmounted(() => {
|
||||
<RouterLink :to="activityRoute(item.id)">
|
||||
مشاهده
|
||||
</RouterLink>
|
||||
<RouterLink :to="activityRoute(item.id, true)">
|
||||
ویرایش
|
||||
</RouterLink>
|
||||
<button v-if="canPromotePrior(item)" type="button" @click="promotePriorActivity(item.id)">
|
||||
انتقال به کار جاری
|
||||
</button>
|
||||
<button v-if="canMarkShare(item)" type="button" @click="complete(item.id)">
|
||||
{{ completeLabel(item) }}
|
||||
</button>
|
||||
<button type="button" class="danger" @click="askDeleteActivity(item.id, item.title)">
|
||||
<button
|
||||
v-if="canDeleteActivity(item)"
|
||||
type="button"
|
||||
class="danger"
|
||||
@click="askDeleteActivity(item.id, item.title)"
|
||||
>
|
||||
حذف
|
||||
</button>
|
||||
</UiDropdownMenu>
|
||||
@@ -1922,18 +1958,11 @@ onUnmounted(() => {
|
||||
<button type="button" role="menuitem" @click="startSelect(boardMenuId)">انتخاب</button>
|
||||
<RouterLink
|
||||
role="menuitem"
|
||||
:to="activityRoute(boardMenuId, false, viewMode === 'board' ? boardMenuStageId : undefined)"
|
||||
:to="activityRoute(boardMenuId, viewMode === 'board' ? boardMenuStageId : undefined)"
|
||||
@click="boardMenuId = null; boardMenuActivity = null; boardMenuStageId = ''"
|
||||
>
|
||||
مشاهده
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
role="menuitem"
|
||||
:to="activityRoute(boardMenuId, true)"
|
||||
@click="boardMenuId = null; boardMenuActivity = null"
|
||||
>
|
||||
ویرایش
|
||||
</RouterLink>
|
||||
<button
|
||||
v-if="boardMenuActivity && canPromotePrior(boardMenuActivity)"
|
||||
type="button"
|
||||
@@ -1943,6 +1972,7 @@ onUnmounted(() => {
|
||||
انتقال به کار جاری
|
||||
</button>
|
||||
<button
|
||||
v-if="canDeleteActivity(boardMenuActivity)"
|
||||
type="button"
|
||||
role="menuitem"
|
||||
class="danger"
|
||||
|
||||
Reference in New Issue
Block a user