refactor(task-list): enhance task row components with depth handling
- Added depth and maxDepth props to TaskRow, TaskRowWithSubtasks, and TitleColumn components to manage nested subtasks more effectively. - Updated AddSubtaskRow to support depth for proper indentation and visual hierarchy. - Improved styling for subtasks based on their depth level, ensuring better visual distinction. - Adjusted task management slice to utilize actual subtask counts from the backend for accurate display.
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
import { useSocket } from '@/socket/socketContext';
|
import { useSocket } from '@/socket/socketContext';
|
||||||
import { ITaskPhase } from '@/types/tasks/taskPhase.types';
|
import { ITaskPhase } from '@/types/tasks/taskPhase.types';
|
||||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
|
||||||
import { Select } from 'antd';
|
import { Select } from 'antd';
|
||||||
|
|
||||||
import { Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
@@ -27,12 +26,6 @@ const TaskDrawerPhaseSelector = ({ phases, task }: TaskDrawerPhaseSelectorProps)
|
|||||||
phase_id: value,
|
phase_id: value,
|
||||||
parent_task: task.parent_task_id || null,
|
parent_task: task.parent_task_id || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// socket?.once(SocketEvents.TASK_PHASE_CHANGE.toString(), () => {
|
|
||||||
// if(list.getCurrentGroup().value === this.list.GROUP_BY_PHASE_VALUE && this.list.isSubtasksIncluded) {
|
|
||||||
// this.list.emitRefreshSubtasksIncluded();
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -41,8 +34,11 @@ const TaskDrawerPhaseSelector = ({ phases, task }: TaskDrawerPhaseSelectorProps)
|
|||||||
allowClear
|
allowClear
|
||||||
placeholder="Select Phase"
|
placeholder="Select Phase"
|
||||||
options={phaseMenuItems}
|
options={phaseMenuItems}
|
||||||
style={{ width: 'fit-content' }}
|
styles={{
|
||||||
dropdownStyle={{ width: 'fit-content' }}
|
root: {
|
||||||
|
width: 'fit-content',
|
||||||
|
},
|
||||||
|
}}
|
||||||
onChange={handlePhaseChange}
|
onChange={handlePhaseChange}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ interface TaskRowProps {
|
|||||||
isSubtask?: boolean;
|
isSubtask?: boolean;
|
||||||
isFirstInGroup?: boolean;
|
isFirstInGroup?: boolean;
|
||||||
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
|
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
|
||||||
|
depth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TaskRow: React.FC<TaskRowProps> = memo(({
|
const TaskRow: React.FC<TaskRowProps> = memo(({
|
||||||
@@ -32,7 +33,8 @@ const TaskRow: React.FC<TaskRowProps> = memo(({
|
|||||||
visibleColumns,
|
visibleColumns,
|
||||||
isSubtask = false,
|
isSubtask = false,
|
||||||
isFirstInGroup = false,
|
isFirstInGroup = false,
|
||||||
updateTaskCustomColumnValue
|
updateTaskCustomColumnValue,
|
||||||
|
depth = 0
|
||||||
}) => {
|
}) => {
|
||||||
// Get task data and selection state from Redux
|
// Get task data and selection state from Redux
|
||||||
const task = useAppSelector(state => selectTaskById(state, taskId));
|
const task = useAppSelector(state => selectTaskById(state, taskId));
|
||||||
@@ -107,6 +109,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({
|
|||||||
handleTaskNameEdit,
|
handleTaskNameEdit,
|
||||||
attributes,
|
attributes,
|
||||||
listeners,
|
listeners,
|
||||||
|
depth,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Memoize style object to prevent unnecessary re-renders
|
// Memoize style object to prevent unnecessary re-renders
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ interface TaskRowWithSubtasksProps {
|
|||||||
}>;
|
}>;
|
||||||
isFirstInGroup?: boolean;
|
isFirstInGroup?: boolean;
|
||||||
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
|
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
|
||||||
|
depth?: number; // Add depth prop to track nesting level
|
||||||
|
maxDepth?: number; // Add maxDepth prop to limit nesting
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddSubtaskRowProps {
|
interface AddSubtaskRowProps {
|
||||||
@@ -32,14 +34,15 @@ interface AddSubtaskRowProps {
|
|||||||
width: string;
|
width: string;
|
||||||
isSticky?: boolean;
|
isSticky?: boolean;
|
||||||
}>;
|
}>;
|
||||||
onSubtaskAdded: () => void; // Simplified - no rowId needed
|
onSubtaskAdded: () => void;
|
||||||
rowId: string; // Unique identifier for this add subtask row
|
rowId: string;
|
||||||
autoFocus?: boolean; // Whether this row should auto-focus on mount
|
autoFocus?: boolean;
|
||||||
isActive?: boolean; // Whether this row should show the input/button
|
isActive?: boolean;
|
||||||
onActivate?: () => void; // Simplified - no rowId needed
|
onActivate?: () => void;
|
||||||
|
depth?: number; // Add depth prop for proper indentation
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
|
const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
|
||||||
parentTaskId,
|
parentTaskId,
|
||||||
projectId,
|
projectId,
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
@@ -47,25 +50,20 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
|
|||||||
rowId,
|
rowId,
|
||||||
autoFocus = false,
|
autoFocus = false,
|
||||||
isActive = true,
|
isActive = true,
|
||||||
onActivate
|
onActivate,
|
||||||
|
depth = 0
|
||||||
}) => {
|
}) => {
|
||||||
const [isAdding, setIsAdding] = useState(autoFocus);
|
const { t } = useTranslation('task-list-table');
|
||||||
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
const [subtaskName, setSubtaskName] = useState('');
|
const [subtaskName, setSubtaskName] = useState('');
|
||||||
const inputRef = useRef<any>(null);
|
const inputRef = useRef<any>(null);
|
||||||
const { socket, connected } = useSocket();
|
|
||||||
const { t } = useTranslation('task-list-table');
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const { socket, connected } = useSocket();
|
||||||
// Get session data for reporter_id and team_id
|
|
||||||
const currentSession = useAuthService().getCurrentSession();
|
const currentSession = useAuthService().getCurrentSession();
|
||||||
|
|
||||||
// Auto-focus when autoFocus prop is true
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoFocus && inputRef.current) {
|
if (autoFocus && inputRef.current) {
|
||||||
setIsAdding(true);
|
inputRef.current.focus();
|
||||||
setTimeout(() => {
|
|
||||||
inputRef.current?.focus();
|
|
||||||
}, 100);
|
|
||||||
}
|
}
|
||||||
}, [autoFocus]);
|
}, [autoFocus]);
|
||||||
|
|
||||||
@@ -142,10 +140,14 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
|
|||||||
<div className="flex items-center h-full" style={baseStyle}>
|
<div className="flex items-center h-full" style={baseStyle}>
|
||||||
<div className="flex items-center w-full h-full">
|
<div className="flex items-center w-full h-full">
|
||||||
{/* Match subtask indentation pattern - tighter spacing */}
|
{/* Match subtask indentation pattern - tighter spacing */}
|
||||||
<div className="w-4" />
|
<div className="w-3" />
|
||||||
|
{/* Add additional indentation for deeper levels - 16px per level */}
|
||||||
|
{Array.from({ length: depth }).map((_, i) => (
|
||||||
|
<div key={i} className="w-4" />
|
||||||
|
))}
|
||||||
<div className="w-2" />
|
<div className="w-2" />
|
||||||
|
|
||||||
{isActive ? (
|
{isActive ? (
|
||||||
!isAdding ? (
|
!isAdding ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -188,7 +190,7 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
|
|||||||
default:
|
default:
|
||||||
return <div style={baseStyle} />;
|
return <div style={baseStyle} />;
|
||||||
}
|
}
|
||||||
}, [isAdding, subtaskName, handleAddSubtask, handleCancel, handleBlur, handleKeyDown, t, isActive, onActivate]);
|
}, [isAdding, subtaskName, handleAddSubtask, handleCancel, handleBlur, handleKeyDown, t, isActive, onActivate, depth]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center min-w-max px-1 py-0.5 hover:bg-gray-50 dark:hover:bg-gray-800 min-h-[36px] border-b border-gray-200 dark:border-gray-700">
|
<div className="flex items-center min-w-max px-1 py-0.5 hover:bg-gray-50 dark:hover:bg-gray-800 min-h-[36px] border-b border-gray-200 dark:border-gray-700">
|
||||||
@@ -203,12 +205,42 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
|
|||||||
|
|
||||||
AddSubtaskRow.displayName = 'AddSubtaskRow';
|
AddSubtaskRow.displayName = 'AddSubtaskRow';
|
||||||
|
|
||||||
|
// Helper function to get background color based on depth
|
||||||
|
const getSubtaskBackgroundColor = (depth: number) => {
|
||||||
|
switch (depth) {
|
||||||
|
case 1:
|
||||||
|
return 'bg-gray-50 dark:bg-gray-800/50';
|
||||||
|
case 2:
|
||||||
|
return 'bg-blue-50 dark:bg-blue-900/20';
|
||||||
|
case 3:
|
||||||
|
return 'bg-green-50 dark:bg-green-900/20';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-50 dark:bg-gray-800/50';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to get border color based on depth
|
||||||
|
const getBorderColor = (depth: number) => {
|
||||||
|
switch (depth) {
|
||||||
|
case 1:
|
||||||
|
return 'border-blue-200 dark:border-blue-700';
|
||||||
|
case 2:
|
||||||
|
return 'border-green-200 dark:border-green-700';
|
||||||
|
case 3:
|
||||||
|
return 'border-purple-200 dark:border-purple-700';
|
||||||
|
default:
|
||||||
|
return 'border-blue-200 dark:border-blue-700';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
|
const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
|
||||||
taskId,
|
taskId,
|
||||||
projectId,
|
projectId,
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
isFirstInGroup = false,
|
isFirstInGroup = false,
|
||||||
updateTaskCustomColumnValue
|
updateTaskCustomColumnValue,
|
||||||
|
depth = 0,
|
||||||
|
maxDepth = 3
|
||||||
}) => {
|
}) => {
|
||||||
const task = useAppSelector(state => selectTaskById(state, taskId));
|
const task = useAppSelector(state => selectTaskById(state, taskId));
|
||||||
const isLoadingSubtasks = useAppSelector(state => selectSubtaskLoading(state, taskId));
|
const isLoadingSubtasks = useAppSelector(state => selectSubtaskLoading(state, taskId));
|
||||||
@@ -223,6 +255,9 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't render subtasks if we've reached the maximum depth
|
||||||
|
const canHaveSubtasks = depth < maxDepth;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Main task row */}
|
{/* Main task row */}
|
||||||
@@ -232,10 +267,12 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
|
|||||||
visibleColumns={visibleColumns}
|
visibleColumns={visibleColumns}
|
||||||
isFirstInGroup={isFirstInGroup}
|
isFirstInGroup={isFirstInGroup}
|
||||||
updateTaskCustomColumnValue={updateTaskCustomColumnValue}
|
updateTaskCustomColumnValue={updateTaskCustomColumnValue}
|
||||||
|
isSubtask={depth > 0}
|
||||||
|
depth={depth}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Subtasks and add subtask row when expanded */}
|
{/* Subtasks and add subtask row when expanded */}
|
||||||
{task.show_sub_tasks && (
|
{canHaveSubtasks && task.show_sub_tasks && (
|
||||||
<>
|
<>
|
||||||
{/* Show loading skeleton while fetching subtasks */}
|
{/* Show loading skeleton while fetching subtasks */}
|
||||||
{isLoadingSubtasks && (
|
{isLoadingSubtasks && (
|
||||||
@@ -244,22 +281,23 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Render existing subtasks when not loading */}
|
{/* Render existing subtasks when not loading - RECURSIVELY */}
|
||||||
{!isLoadingSubtasks && task.sub_tasks?.map((subtask: Task) => (
|
{!isLoadingSubtasks && task.sub_tasks?.map((subtask: Task) => (
|
||||||
<div key={subtask.id} className="bg-gray-50 dark:bg-gray-800/50 border-l-2 border-blue-200 dark:border-blue-700">
|
<div key={subtask.id} className={`${getSubtaskBackgroundColor(depth + 1)} border-l-2 ${getBorderColor(depth + 1)}`}>
|
||||||
<TaskRow
|
<TaskRowWithSubtasks
|
||||||
taskId={subtask.id}
|
taskId={subtask.id}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
visibleColumns={visibleColumns}
|
visibleColumns={visibleColumns}
|
||||||
isSubtask={true}
|
|
||||||
updateTaskCustomColumnValue={updateTaskCustomColumnValue}
|
updateTaskCustomColumnValue={updateTaskCustomColumnValue}
|
||||||
|
depth={depth + 1}
|
||||||
|
maxDepth={maxDepth}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Add subtask row - only show when not loading */}
|
{/* Add subtask row - only show when not loading */}
|
||||||
{!isLoadingSubtasks && (
|
{!isLoadingSubtasks && (
|
||||||
<div className="bg-gray-50 dark:bg-gray-800/50 border-l-2 border-blue-200 dark:border-blue-700">
|
<div className={`${getSubtaskBackgroundColor(depth + 1)} border-l-2 ${getBorderColor(depth + 1)}`}>
|
||||||
<AddSubtaskRow
|
<AddSubtaskRow
|
||||||
parentTaskId={taskId}
|
parentTaskId={taskId}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
@@ -267,8 +305,9 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
|
|||||||
onSubtaskAdded={handleSubtaskAdded}
|
onSubtaskAdded={handleSubtaskAdded}
|
||||||
rowId={`add-subtask-${taskId}`}
|
rowId={`add-subtask-${taskId}`}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
isActive={true} // Always show the add subtask row
|
isActive={true}
|
||||||
onActivate={undefined} // Not needed anymore
|
onActivate={undefined}
|
||||||
|
depth={depth + 1}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ interface TitleColumnProps {
|
|||||||
onEditTaskName: (editing: boolean) => void;
|
onEditTaskName: (editing: boolean) => void;
|
||||||
onTaskNameChange: (name: string) => void;
|
onTaskNameChange: (name: string) => void;
|
||||||
onTaskNameSave: () => void;
|
onTaskNameSave: () => void;
|
||||||
|
depth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TitleColumn: React.FC<TitleColumnProps> = memo(({
|
export const TitleColumn: React.FC<TitleColumnProps> = memo(({
|
||||||
@@ -36,7 +37,8 @@ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
|
|||||||
taskName,
|
taskName,
|
||||||
onEditTaskName,
|
onEditTaskName,
|
||||||
onTaskNameChange,
|
onTaskNameChange,
|
||||||
onTaskNameSave
|
onTaskNameSave,
|
||||||
|
depth = 0
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { socket, connected } = useSocket();
|
const { socket, connected } = useSocket();
|
||||||
@@ -151,10 +153,15 @@ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
|
|||||||
<>
|
<>
|
||||||
<div className="flex items-center flex-1 min-w-0">
|
<div className="flex items-center flex-1 min-w-0">
|
||||||
{/* Indentation for subtasks - tighter spacing */}
|
{/* Indentation for subtasks - tighter spacing */}
|
||||||
{isSubtask && <div className="w-4 flex-shrink-0" />}
|
{isSubtask && <div className="w-3 flex-shrink-0" />}
|
||||||
|
|
||||||
{/* Expand/Collapse button - only show for parent tasks */}
|
{/* Additional indentation for deeper levels - 16px per level */}
|
||||||
{!isSubtask && (
|
{Array.from({ length: depth }).map((_, i) => (
|
||||||
|
<div key={i} className="w-4 flex-shrink-0" />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Expand/Collapse button - show for any task that can have sub-tasks */}
|
||||||
|
{depth < 2 && ( // Only show if not at maximum depth (can still have children)
|
||||||
<button
|
<button
|
||||||
onClick={handleToggleExpansion}
|
onClick={handleToggleExpansion}
|
||||||
className={`flex h-4 w-4 items-center justify-center rounded-sm text-xs mr-1 hover:border hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 hover:scale-110 transition-all duration-300 ease-out flex-shrink-0 ${
|
className={`flex h-4 w-4 items-center justify-center rounded-sm text-xs mr-1 hover:border hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 hover:scale-110 transition-all duration-300 ease-out flex-shrink-0 ${
|
||||||
@@ -202,8 +209,8 @@ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
|
|||||||
|
|
||||||
{/* Indicators container - flex-shrink-0 to prevent compression */}
|
{/* Indicators container - flex-shrink-0 to prevent compression */}
|
||||||
<div className="flex items-center gap-1 flex-shrink-0">
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
{/* Subtask count indicator - only show if count > 0 */}
|
{/* Subtask count indicator - show for any task that can have sub-tasks */}
|
||||||
{!isSubtask && task.sub_tasks_count != null && task.sub_tasks_count > 0 && (
|
{depth < 2 && task.sub_tasks_count != null && task.sub_tasks_count > 0 && (
|
||||||
<Tooltip title={t(`indicators.tooltips.subtasks${task.sub_tasks_count === 1 ? '' : '_plural'}`, { count: task.sub_tasks_count })}>
|
<Tooltip title={t(`indicators.tooltips.subtasks${task.sub_tasks_count === 1 ? '' : '_plural'}`, { count: task.sub_tasks_count })}>
|
||||||
<div className="flex items-center gap-1 px-1.5 py-0.5 bg-blue-50 dark:bg-blue-900/20 rounded text-xs">
|
<div className="flex items-center gap-1 px-1.5 py-0.5 bg-blue-50 dark:bg-blue-900/20 rounded text-xs">
|
||||||
<span className="text-blue-600 dark:text-blue-400 font-medium">
|
<span className="text-blue-600 dark:text-blue-400 font-medium">
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ interface UseTaskRowColumnsProps {
|
|||||||
// Drag and drop
|
// Drag and drop
|
||||||
attributes: any;
|
attributes: any;
|
||||||
listeners: any;
|
listeners: any;
|
||||||
|
|
||||||
|
// Depth for nested subtasks
|
||||||
|
depth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTaskRowColumns = ({
|
export const useTaskRowColumns = ({
|
||||||
@@ -84,6 +87,7 @@ export const useTaskRowColumns = ({
|
|||||||
handleTaskNameEdit,
|
handleTaskNameEdit,
|
||||||
attributes,
|
attributes,
|
||||||
listeners,
|
listeners,
|
||||||
|
depth = 0,
|
||||||
}: UseTaskRowColumnsProps) => {
|
}: UseTaskRowColumnsProps) => {
|
||||||
|
|
||||||
const renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number) => {
|
const renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number) => {
|
||||||
@@ -128,6 +132,7 @@ export const useTaskRowColumns = ({
|
|||||||
onEditTaskName={setEditTaskName}
|
onEditTaskName={setEditTaskName}
|
||||||
onTaskNameChange={setTaskName}
|
onTaskNameChange={setTaskName}
|
||||||
onTaskNameSave={handleTaskNameSave}
|
onTaskNameSave={handleTaskNameSave}
|
||||||
|
depth={depth}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1010,7 +1010,7 @@ const taskManagementSlice = createSlice({
|
|||||||
order: subtask.sort_order || subtask.order || 0,
|
order: subtask.sort_order || subtask.order || 0,
|
||||||
parent_task_id: parentTaskId,
|
parent_task_id: parentTaskId,
|
||||||
is_sub_task: true,
|
is_sub_task: true,
|
||||||
sub_tasks_count: 0,
|
sub_tasks_count: subtask.sub_tasks_count || 0, // Use actual count from backend
|
||||||
show_sub_tasks: false,
|
show_sub_tasks: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user