fix: bring back the original list tasks command
This commit is contained in:
@@ -24,6 +24,9 @@ import {
|
|||||||
calculateSubtaskStatistics,
|
calculateSubtaskStatistics,
|
||||||
calculateDependencyStatistics,
|
calculateDependencyStatistics,
|
||||||
getPriorityBreakdown,
|
getPriorityBreakdown,
|
||||||
|
displayRecommendedNextTask,
|
||||||
|
getTaskDescription,
|
||||||
|
displaySuggestedNextSteps,
|
||||||
type NextTaskInfo
|
type NextTaskInfo
|
||||||
} from '../ui/index.js';
|
} from '../ui/index.js';
|
||||||
|
|
||||||
@@ -284,16 +287,40 @@ export class ListTasksCommand extends Command {
|
|||||||
const nextTask = this.findNextTask(tasks);
|
const nextTask = this.findNextTask(tasks);
|
||||||
|
|
||||||
// Display dashboard boxes
|
// Display dashboard boxes
|
||||||
displayDashboards(taskStats, subtaskStats, priorityBreakdown, depStats, nextTask);
|
displayDashboards(
|
||||||
|
taskStats,
|
||||||
|
subtaskStats,
|
||||||
|
priorityBreakdown,
|
||||||
|
depStats,
|
||||||
|
nextTask
|
||||||
|
);
|
||||||
|
|
||||||
// Task table
|
// Task table - no title, just show the table directly
|
||||||
console.log(chalk.blue.bold(`\n📋 Tasks (${tasks.length}):\n`));
|
|
||||||
console.log(
|
console.log(
|
||||||
ui.createTaskTable(tasks, {
|
ui.createTaskTable(tasks, {
|
||||||
showSubtasks: withSubtasks,
|
showSubtasks: withSubtasks,
|
||||||
showDependencies: true
|
showDependencies: true,
|
||||||
|
showComplexity: true // Enable complexity column
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Display recommended next task section immediately after table
|
||||||
|
if (nextTask) {
|
||||||
|
// Find the full task object to get description
|
||||||
|
const fullTask = tasks.find(t => String(t.id) === String(nextTask.id));
|
||||||
|
const description = fullTask ? getTaskDescription(fullTask) : undefined;
|
||||||
|
|
||||||
|
displayRecommendedNextTask({
|
||||||
|
...nextTask,
|
||||||
|
status: 'pending', // Next task is typically pending
|
||||||
|
description
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
displayRecommendedNextTask(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display suggested next steps at the end
|
||||||
|
displaySuggestedNextSteps();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -317,12 +344,12 @@ export class ListTasksCommand extends Command {
|
|||||||
|
|
||||||
// Build set of completed task IDs (including subtasks)
|
// Build set of completed task IDs (including subtasks)
|
||||||
const completedIds = new Set<string>();
|
const completedIds = new Set<string>();
|
||||||
tasks.forEach(t => {
|
tasks.forEach((t) => {
|
||||||
if (t.status === 'done' || t.status === 'completed') {
|
if (t.status === 'done' || t.status === 'completed') {
|
||||||
completedIds.add(String(t.id));
|
completedIds.add(String(t.id));
|
||||||
}
|
}
|
||||||
if (t.subtasks) {
|
if (t.subtasks) {
|
||||||
t.subtasks.forEach(st => {
|
t.subtasks.forEach((st) => {
|
||||||
if (st.status === 'done' || st.status === 'completed') {
|
if (st.status === 'done' || st.status === 'completed') {
|
||||||
completedIds.add(`${t.id}.${st.id}`);
|
completedIds.add(`${t.id}.${st.id}`);
|
||||||
}
|
}
|
||||||
@@ -334,14 +361,17 @@ export class ListTasksCommand extends Command {
|
|||||||
const candidateSubtasks: NextTaskInfo[] = [];
|
const candidateSubtasks: NextTaskInfo[] = [];
|
||||||
|
|
||||||
tasks
|
tasks
|
||||||
.filter(t => t.status === 'in-progress' && t.subtasks && t.subtasks.length > 0)
|
.filter(
|
||||||
.forEach(parent => {
|
(t) => t.status === 'in-progress' && t.subtasks && t.subtasks.length > 0
|
||||||
parent.subtasks!.forEach(st => {
|
)
|
||||||
|
.forEach((parent) => {
|
||||||
|
parent.subtasks!.forEach((st) => {
|
||||||
const stStatus = (st.status || 'pending').toLowerCase();
|
const stStatus = (st.status || 'pending').toLowerCase();
|
||||||
if (stStatus !== 'pending' && stStatus !== 'in-progress') return;
|
if (stStatus !== 'pending' && stStatus !== 'in-progress') return;
|
||||||
|
|
||||||
// Check if dependencies are satisfied
|
// Check if dependencies are satisfied
|
||||||
const fullDeps = st.dependencies?.map(d => {
|
const fullDeps =
|
||||||
|
st.dependencies?.map((d) => {
|
||||||
// Handle both numeric and string IDs
|
// Handle both numeric and string IDs
|
||||||
if (typeof d === 'string' && d.includes('.')) {
|
if (typeof d === 'string' && d.includes('.')) {
|
||||||
return d;
|
return d;
|
||||||
@@ -349,15 +379,16 @@ export class ListTasksCommand extends Command {
|
|||||||
return `${parent.id}.${d}`;
|
return `${parent.id}.${d}`;
|
||||||
}) ?? [];
|
}) ?? [];
|
||||||
|
|
||||||
const depsSatisfied = fullDeps.length === 0 ||
|
const depsSatisfied =
|
||||||
fullDeps.every(depId => completedIds.has(String(depId)));
|
fullDeps.length === 0 ||
|
||||||
|
fullDeps.every((depId) => completedIds.has(String(depId)));
|
||||||
|
|
||||||
if (depsSatisfied) {
|
if (depsSatisfied) {
|
||||||
candidateSubtasks.push({
|
candidateSubtasks.push({
|
||||||
id: `${parent.id}.${st.id}`,
|
id: `${parent.id}.${st.id}`,
|
||||||
title: st.title || `Subtask ${st.id}`,
|
title: st.title || `Subtask ${st.id}`,
|
||||||
priority: st.priority || parent.priority || 'medium',
|
priority: st.priority || parent.priority || 'medium',
|
||||||
dependencies: fullDeps.map(d => String(d))
|
dependencies: fullDeps.map((d) => String(d))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -380,15 +411,16 @@ export class ListTasksCommand extends Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to finding eligible top-level tasks
|
// Fall back to finding eligible top-level tasks
|
||||||
const eligibleTasks = tasks.filter(task => {
|
const eligibleTasks = tasks.filter((task) => {
|
||||||
// Skip non-eligible statuses
|
// Skip non-eligible statuses
|
||||||
const status = (task.status || 'pending').toLowerCase();
|
const status = (task.status || 'pending').toLowerCase();
|
||||||
if (status !== 'pending' && status !== 'in-progress') return false;
|
if (status !== 'pending' && status !== 'in-progress') return false;
|
||||||
|
|
||||||
// Check dependencies
|
// Check dependencies
|
||||||
const deps = task.dependencies || [];
|
const deps = task.dependencies || [];
|
||||||
const depsSatisfied = deps.length === 0 ||
|
const depsSatisfied =
|
||||||
deps.every(depId => completedIds.has(String(depId)));
|
deps.length === 0 ||
|
||||||
|
deps.every((depId) => completedIds.has(String(depId)));
|
||||||
|
|
||||||
return depsSatisfied;
|
return depsSatisfied;
|
||||||
});
|
});
|
||||||
@@ -416,7 +448,7 @@ export class ListTasksCommand extends Command {
|
|||||||
id: nextTask.id,
|
id: nextTask.id,
|
||||||
title: nextTask.title,
|
title: nextTask.title,
|
||||||
priority: nextTask.priority,
|
priority: nextTask.priority,
|
||||||
dependencies: nextTask.dependencies?.map(d => String(d))
|
dependencies: nextTask.dependencies?.map((d) => String(d))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,5 @@
|
|||||||
|
|
||||||
export * from './header.component.js';
|
export * from './header.component.js';
|
||||||
export * from './dashboard.component.js';
|
export * from './dashboard.component.js';
|
||||||
|
export * from './next-task.component.js';
|
||||||
|
export * from './suggested-steps.component.js';
|
||||||
134
apps/cli/src/ui/components/next-task.component.ts
Normal file
134
apps/cli/src/ui/components/next-task.component.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Next task recommendation component
|
||||||
|
* Displays detailed information about the recommended next task
|
||||||
|
*/
|
||||||
|
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import boxen from 'boxen';
|
||||||
|
import type { Task } from '@tm/core/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Next task display options
|
||||||
|
*/
|
||||||
|
export interface NextTaskDisplayOptions {
|
||||||
|
id: string | number;
|
||||||
|
title: string;
|
||||||
|
priority?: string;
|
||||||
|
status?: string;
|
||||||
|
dependencies?: (string | number)[];
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the recommended next task section
|
||||||
|
*/
|
||||||
|
export function displayRecommendedNextTask(
|
||||||
|
task: NextTaskDisplayOptions | undefined
|
||||||
|
): void {
|
||||||
|
if (!task) {
|
||||||
|
// If no task available, show a message
|
||||||
|
console.log(
|
||||||
|
boxen(
|
||||||
|
chalk.yellow(
|
||||||
|
'No tasks available to work on. All tasks are either completed, blocked by dependencies, or in progress.'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
padding: 1,
|
||||||
|
borderStyle: 'round',
|
||||||
|
borderColor: 'yellow',
|
||||||
|
title: '⚠ NO TASKS AVAILABLE ⚠',
|
||||||
|
titleAlignment: 'center'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the content for the next task box
|
||||||
|
const content = [];
|
||||||
|
|
||||||
|
// Task header with ID and title
|
||||||
|
content.push(
|
||||||
|
`🔥 ${chalk.hex('#FF8800').bold('Next Task to Work On:')} ${chalk.yellow(`#${task.id}`)}${chalk.hex('#FF8800').bold(` - ${task.title}`)}`
|
||||||
|
);
|
||||||
|
content.push('');
|
||||||
|
|
||||||
|
// Priority and Status line
|
||||||
|
const statusLine = [];
|
||||||
|
if (task.priority) {
|
||||||
|
const priorityColor =
|
||||||
|
task.priority === 'high'
|
||||||
|
? chalk.red
|
||||||
|
: task.priority === 'medium'
|
||||||
|
? chalk.yellow
|
||||||
|
: chalk.gray;
|
||||||
|
statusLine.push(`Priority: ${priorityColor.bold(task.priority)}`);
|
||||||
|
}
|
||||||
|
if (task.status) {
|
||||||
|
const statusDisplay =
|
||||||
|
task.status === 'pending'
|
||||||
|
? chalk.yellow('○ pending')
|
||||||
|
: task.status === 'in-progress'
|
||||||
|
? chalk.blue('▶ in-progress')
|
||||||
|
: chalk.gray(task.status);
|
||||||
|
statusLine.push(`Status: ${statusDisplay}`);
|
||||||
|
}
|
||||||
|
content.push(statusLine.join(' '));
|
||||||
|
|
||||||
|
// Dependencies
|
||||||
|
const depsDisplay =
|
||||||
|
!task.dependencies || task.dependencies.length === 0
|
||||||
|
? chalk.gray('None')
|
||||||
|
: chalk.cyan(task.dependencies.join(', '));
|
||||||
|
content.push(`Dependencies: ${depsDisplay}`);
|
||||||
|
|
||||||
|
// Description if available
|
||||||
|
if (task.description) {
|
||||||
|
content.push('');
|
||||||
|
content.push(`Description: ${chalk.white(task.description)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action commands
|
||||||
|
content.push('');
|
||||||
|
content.push(
|
||||||
|
`${chalk.cyan('Start working:')} ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}`
|
||||||
|
);
|
||||||
|
content.push(
|
||||||
|
`${chalk.cyan('View details:')} ${chalk.yellow(`task-master show ${task.id}`)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Display in a styled box with orange border
|
||||||
|
console.log(
|
||||||
|
boxen(content.join('\n'), {
|
||||||
|
padding: 1,
|
||||||
|
margin: { top: 1, bottom: 1 },
|
||||||
|
borderStyle: 'round',
|
||||||
|
borderColor: '#FFA500', // Orange color
|
||||||
|
title: chalk.hex('#FFA500')('⚡ RECOMMENDED NEXT TASK ⚡'),
|
||||||
|
titleAlignment: 'center',
|
||||||
|
width: process.stdout.columns * 0.97,
|
||||||
|
fullscreen: false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get task description from the full task object
|
||||||
|
*/
|
||||||
|
export function getTaskDescription(task: Task): string | undefined {
|
||||||
|
// Try to get description from the task
|
||||||
|
// This could be from task.description or the first line of task.details
|
||||||
|
if ('description' in task && task.description) {
|
||||||
|
return task.description as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('details' in task && task.details) {
|
||||||
|
// Take first sentence or line from details
|
||||||
|
const details = task.details as string;
|
||||||
|
const firstLine = details.split('\n')[0];
|
||||||
|
const firstSentence = firstLine.split('.')[0];
|
||||||
|
return firstSentence;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
31
apps/cli/src/ui/components/suggested-steps.component.ts
Normal file
31
apps/cli/src/ui/components/suggested-steps.component.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview Suggested next steps component
|
||||||
|
* Displays helpful command suggestions at the end of the list
|
||||||
|
*/
|
||||||
|
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import boxen from 'boxen';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display suggested next steps section
|
||||||
|
*/
|
||||||
|
export function displaySuggestedNextSteps(): void {
|
||||||
|
const steps = [
|
||||||
|
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master next')} to see what to work on next`,
|
||||||
|
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks`,
|
||||||
|
`${chalk.cyan('3.')} Run ${chalk.yellow('task-master set-status --id=<id> --status=done')} to mark a task as complete`
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
boxen(
|
||||||
|
chalk.white.bold('Suggested Next Steps:') + '\n\n' + steps.join('\n'),
|
||||||
|
{
|
||||||
|
padding: 1,
|
||||||
|
margin: { top: 0, bottom: 1 },
|
||||||
|
borderStyle: 'round',
|
||||||
|
borderColor: 'gray',
|
||||||
|
width: process.stdout.columns * 0.97
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,19 +18,39 @@ export function getStatusWithColor(
|
|||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
done: {
|
done: {
|
||||||
color: chalk.green,
|
color: chalk.green,
|
||||||
icon: String.fromCharCode(8730),
|
icon: '✓',
|
||||||
tableIcon: String.fromCharCode(8730)
|
tableIcon: '✓'
|
||||||
}, // √
|
},
|
||||||
pending: { color: chalk.yellow, icon: 'o', tableIcon: 'o' },
|
pending: {
|
||||||
|
color: chalk.yellow,
|
||||||
|
icon: '○',
|
||||||
|
tableIcon: '○'
|
||||||
|
},
|
||||||
'in-progress': {
|
'in-progress': {
|
||||||
color: chalk.hex('#FFA500'),
|
color: chalk.hex('#FFA500'),
|
||||||
icon: String.fromCharCode(9654),
|
icon: '▶',
|
||||||
tableIcon: '>'
|
tableIcon: '▶'
|
||||||
}, // ▶
|
},
|
||||||
deferred: { color: chalk.gray, icon: 'x', tableIcon: 'x' },
|
deferred: {
|
||||||
blocked: { color: chalk.red, icon: '!', tableIcon: '!' },
|
color: chalk.gray,
|
||||||
review: { color: chalk.magenta, icon: '?', tableIcon: '?' },
|
icon: 'x',
|
||||||
cancelled: { color: chalk.gray, icon: 'X', tableIcon: 'X' }
|
tableIcon: 'x'
|
||||||
|
},
|
||||||
|
review: {
|
||||||
|
color: chalk.magenta,
|
||||||
|
icon: '?',
|
||||||
|
tableIcon: '?'
|
||||||
|
},
|
||||||
|
cancelled: {
|
||||||
|
color: chalk.gray,
|
||||||
|
icon: 'x',
|
||||||
|
tableIcon: 'x'
|
||||||
|
},
|
||||||
|
blocked: {
|
||||||
|
color: chalk.red,
|
||||||
|
icon: '!',
|
||||||
|
tableIcon: '!'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = statusConfig[status] || {
|
const config = statusConfig[status] || {
|
||||||
@@ -39,18 +59,7 @@ export function getStatusWithColor(
|
|||||||
tableIcon: 'X'
|
tableIcon: 'X'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use simple ASCII characters for stable display
|
const icon = forTable ? config.tableIcon : config.icon;
|
||||||
const simpleIcons = {
|
|
||||||
done: String.fromCharCode(8730), // √
|
|
||||||
pending: 'o',
|
|
||||||
'in-progress': '>',
|
|
||||||
deferred: 'x',
|
|
||||||
blocked: '!',
|
|
||||||
review: '?',
|
|
||||||
cancelled: 'X'
|
|
||||||
};
|
|
||||||
|
|
||||||
const icon = forTable ? simpleIcons[status] || 'X' : config.icon;
|
|
||||||
return config.color(`${icon} ${status}`);
|
return config.color(`${icon} ${status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,10 +254,24 @@ export function createTaskTable(
|
|||||||
} = options || {};
|
} = options || {};
|
||||||
|
|
||||||
// Calculate dynamic column widths based on terminal width
|
// Calculate dynamic column widths based on terminal width
|
||||||
const terminalWidth = process.stdout.columns || 100;
|
const terminalWidth = process.stdout.columns * 0.9 || 100;
|
||||||
|
// Adjust column widths to better match the original layout
|
||||||
const baseColWidths = showComplexity
|
const baseColWidths = showComplexity
|
||||||
? [8, Math.floor(terminalWidth * 0.35), 18, 12, 15, 12] // ID, Title, Status, Priority, Dependencies, Complexity
|
? [
|
||||||
: [8, Math.floor(terminalWidth * 0.4), 18, 12, 20]; // ID, Title, Status, Priority, Dependencies
|
Math.floor(terminalWidth * 0.06),
|
||||||
|
Math.floor(terminalWidth * 0.4),
|
||||||
|
Math.floor(terminalWidth * 0.15),
|
||||||
|
Math.floor(terminalWidth * 0.12),
|
||||||
|
Math.floor(terminalWidth * 0.2),
|
||||||
|
Math.floor(terminalWidth * 0.12)
|
||||||
|
] // ID, Title, Status, Priority, Dependencies, Complexity
|
||||||
|
: [
|
||||||
|
Math.floor(terminalWidth * 0.08),
|
||||||
|
Math.floor(terminalWidth * 0.4),
|
||||||
|
Math.floor(terminalWidth * 0.18),
|
||||||
|
Math.floor(terminalWidth * 0.12),
|
||||||
|
Math.floor(terminalWidth * 0.2)
|
||||||
|
]; // ID, Title, Status, Priority, Dependencies
|
||||||
|
|
||||||
const headers = [
|
const headers = [
|
||||||
chalk.blue.bold('ID'),
|
chalk.blue.bold('ID'),
|
||||||
@@ -284,11 +307,19 @@ export function createTaskTable(
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (showDependencies) {
|
if (showDependencies) {
|
||||||
row.push(formatDependenciesWithStatus(task.dependencies, tasks));
|
// For table display, show simple format without status icons
|
||||||
|
if (!task.dependencies || task.dependencies.length === 0) {
|
||||||
|
row.push(chalk.gray('None'));
|
||||||
|
} else {
|
||||||
|
row.push(
|
||||||
|
chalk.cyan(task.dependencies.map((d) => String(d)).join(', '))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showComplexity && 'complexity' in task) {
|
if (showComplexity) {
|
||||||
row.push(getComplexityWithColor(task.complexity as number | string));
|
// Show N/A if no complexity score
|
||||||
|
row.push(chalk.gray('N/A'));
|
||||||
}
|
}
|
||||||
|
|
||||||
table.push(row);
|
table.push(row);
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import fs from 'fs';
|
import packageJson from '../../package.json' with { type: 'json' };
|
||||||
import path from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { log } from '../../scripts/modules/utils.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the version from the nearest package.json relative to this file.
|
* Reads the version from the nearest package.json relative to this file.
|
||||||
@@ -9,27 +6,5 @@ import { log } from '../../scripts/modules/utils.js';
|
|||||||
* @returns {string} The version string or 'unknown'.
|
* @returns {string} The version string or 'unknown'.
|
||||||
*/
|
*/
|
||||||
export function getTaskMasterVersion() {
|
export function getTaskMasterVersion() {
|
||||||
let version = 'unknown';
|
return packageJson.version || 'unknown';
|
||||||
try {
|
|
||||||
// Get the directory of the current module (getPackageVersion.js)
|
|
||||||
const currentModuleFilename = fileURLToPath(import.meta.url);
|
|
||||||
const currentModuleDirname = path.dirname(currentModuleFilename);
|
|
||||||
// Construct the path to package.json relative to this file (../../package.json)
|
|
||||||
const packageJsonPath = path.join(
|
|
||||||
currentModuleDirname,
|
|
||||||
'..',
|
|
||||||
'..',
|
|
||||||
'package.json'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fs.existsSync(packageJsonPath)) {
|
|
||||||
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
|
|
||||||
const packageJson = JSON.parse(packageJsonContent);
|
|
||||||
version = packageJson.version;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Silently fall back to default version
|
|
||||||
log('warn', 'Could not read own package.json for version info.', error);
|
|
||||||
}
|
|
||||||
return version;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user