mirror of
https://github.com/Gimanh/taskview-community.git
synced 2026-09-12 05:49:01 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3020e41b3a | |||
| 8887d5e3e1 | |||
| 0925547f8d | |||
| c6b769a476 | |||
| 3c49941693 | |||
| 1d79dad5c8 | |||
| 245adf204f | |||
| 08da93bd40 | |||
| 317b32edea |
@@ -5,6 +5,12 @@
|
||||
TaskView is a self-hosted project and task management platform focused on clarity, ownership, and control.
|
||||
TaskView is built for teams that want a transparent, self-hosted alternative to SaaS task managers.
|
||||
|
||||
## Apps
|
||||
* [Docs](https://taskview.tech/docs/)
|
||||
* [Web](https://app.taskview.tech/)
|
||||
* [iOS](https://apps.apple.com/lk/app/taskview-todo-list-tasks/id6499107867)
|
||||
* [Android](https://play.google.com/store/apps/details?id=com.handscreamgnl.taskview.app&hl=en)
|
||||
|
||||
It is designed for teams and individuals who want:
|
||||
- full control over their data
|
||||
- transparent architecture
|
||||
@@ -129,9 +135,9 @@ Make sure the image versions match the version defined in the root package.json.
|
||||
## Roadmap
|
||||
|
||||
- Plugin / extension system
|
||||
- Migrate to NuxtUI or similar ui library
|
||||
- [X] Migrate to NuxtUI or similar ui library
|
||||
- Enterprise SSO and identity integrations
|
||||
- Redesign
|
||||
- [X] Redesign
|
||||
- Desktop version
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "taskview-ce-api-server",
|
||||
"version": "1.20.4",
|
||||
"version": "1.20.7",
|
||||
"scripts": {
|
||||
"dev": "bun run --watch ./server.ts",
|
||||
"start": "NODE_ENV=production node ./dist/taskview-server.js",
|
||||
|
||||
@@ -648,6 +648,7 @@ export class TasksRepository {
|
||||
eq(TasksSchema.goalId, goalId),
|
||||
columnId === null ? isNull(TasksSchema.statusId) : eq(TasksSchema.statusId, columnId),
|
||||
isNotNull(TasksSchema.kanbanOrder),
|
||||
isNull(TasksSchema.parentId),
|
||||
];
|
||||
if (cursor !== null) {
|
||||
conditions.push(gt(TasksSchema.kanbanOrder, cursor));
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "taskview-ce-monorepo",
|
||||
"version": "1.20.4",
|
||||
"version": "1.20.7",
|
||||
"private": true,
|
||||
"description": "TaskView CE monorepo containing web, API, and packages",
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,59 +1,570 @@
|
||||
# TaskView API
|
||||
|
||||
Библиотека для работы с TaskView API.
|
||||
TypeScript/JavaScript SDK for the [TaskView](https://taskview.tech) API. Provides a typed interface for managing goals, task lists, tasks, tags, kanban boards, team collaboration, and task dependencies.
|
||||
|
||||
## Установка
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install taskview-api
|
||||
npm install taskview-api axios
|
||||
```
|
||||
|
||||
## Использование
|
||||
`axios` is a peer dependency — you provide your own configured instance.
|
||||
|
||||
```javascript
|
||||
import { setupCounter } from 'taskview-api'
|
||||
## Quick Start
|
||||
|
||||
// Использование функции setupCounter
|
||||
const button = document.querySelector('#counter')
|
||||
setupCounter(button)
|
||||
```typescript
|
||||
import axios from 'axios';
|
||||
import { TvApi } from 'taskview-api';
|
||||
|
||||
const $axios = axios.create({
|
||||
baseURL: 'https://api.taskview.app',
|
||||
});
|
||||
$axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
|
||||
|
||||
const api = new TvApi($axios);
|
||||
```
|
||||
|
||||
## Разработка
|
||||
You can change the base URL at any time:
|
||||
|
||||
### Установка зависимостей
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```typescript
|
||||
api.setBaseUrl('https://other-api.example.com');
|
||||
```
|
||||
|
||||
### Запуск в режиме разработки
|
||||
## Authentication
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
All API requests (except auth endpoints) require a Bearer token in the `Authorization` header. Below is how to obtain and manage tokens.
|
||||
|
||||
### Login
|
||||
|
||||
**With email or username and password:**
|
||||
|
||||
```typescript
|
||||
const response = await $axios.post('/module/auth/login', {
|
||||
login: 'user@example.com', // email or username
|
||||
password: 'securePassword123',
|
||||
});
|
||||
|
||||
const { access, refresh, userData } = response.data;
|
||||
```
|
||||
|
||||
### Сборка библиотеки
|
||||
**Passwordless login (email code):**
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```typescript
|
||||
// 1. Request a login code
|
||||
await $axios.post('/module/auth/send-login-code', {
|
||||
email: 'user@example.com',
|
||||
});
|
||||
|
||||
// 2. User receives a code via email, then submit it
|
||||
const response = await $axios.post('/module/auth/login-by-code', {
|
||||
email: 'user@example.com',
|
||||
code: '123456',
|
||||
});
|
||||
|
||||
const { access, refresh, userData } = response.data;
|
||||
```
|
||||
|
||||
### Проверка типов
|
||||
### Using Tokens
|
||||
|
||||
```bash
|
||||
npm run type-check
|
||||
Once you have the access token, pass it to `TvApi` via the axios instance:
|
||||
|
||||
```typescript
|
||||
import axios from 'axios';
|
||||
import { TvApi } from 'taskview-api';
|
||||
|
||||
const $axios = axios.create({
|
||||
baseURL: 'https://api.taskview.tech',
|
||||
});
|
||||
$axios.defaults.headers.common['Authorization'] = `Bearer ${access}`;
|
||||
|
||||
const api = new TvApi($axios);
|
||||
```
|
||||
|
||||
## Структура проекта
|
||||
Store the refresh token securely (e.g. `httpOnly` cookie or secure storage). The access token has a short lifetime; the refresh token lives longer (up to 30 days).
|
||||
|
||||
- `src/index.ts` - основной файл экспорта библиотеки
|
||||
- `src/counter.ts` - модуль с функцией счетчика
|
||||
- `dist/` - собранные файлы библиотеки
|
||||
### Refreshing Tokens
|
||||
|
||||
## Форматы сборки
|
||||
When the access token expires the server returns `401 Unauthorized`. Use the refresh token to obtain a new pair:
|
||||
|
||||
Библиотека собирается в следующих форматах:
|
||||
- ES Module (`.es.js`)
|
||||
- CommonJS (`.cjs.js`)
|
||||
- UMD (`.umd.js`)
|
||||
- TypeScript типы (`.d.ts`)
|
||||
```typescript
|
||||
const response = await $axios.post('/module/auth/refresh/token', {
|
||||
refreshToken: refresh,
|
||||
});
|
||||
|
||||
const { access: newAccess, refresh: newRefresh } = response.data;
|
||||
|
||||
// Update the axios header with the new access token
|
||||
$axios.defaults.headers.common['Authorization'] = `Bearer ${newAccess}`;
|
||||
```
|
||||
|
||||
### Logout
|
||||
|
||||
```typescript
|
||||
await $axios.post('/module/auth/logout');
|
||||
// Clear stored tokens on the client side
|
||||
```
|
||||
|
||||
|
||||
### OAuth Providers
|
||||
|
||||
TaskView supports OAuth login via **Google**, **GitHub**, and **Apple**. Redirect the user to:
|
||||
|
||||
```
|
||||
GET /module/auth/provider/google
|
||||
GET /module/auth/provider/github
|
||||
GET /module/auth/provider/apple
|
||||
```
|
||||
|
||||
After successful authentication the user is redirected back with a login code that can be exchanged for tokens via the `login-by-code` endpoint.
|
||||
|
||||
### JWT Payload Structure
|
||||
|
||||
The decoded access token contains:
|
||||
|
||||
```typescript
|
||||
{
|
||||
exp: number; // Expiration timestamp
|
||||
id: number; // Token ID
|
||||
type: 'jwt';
|
||||
userData: {
|
||||
id: number; // User ID
|
||||
email: string;
|
||||
login: string;
|
||||
permissions: {
|
||||
[key: string]: {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Modules
|
||||
|
||||
The `TvApi` instance exposes the following modules:
|
||||
|
||||
| Module | Access | Description |
|
||||
|---------------------|----------------------|----------------------------------------|
|
||||
| Goals | `api.goals` | Create, update, delete, fetch goals |
|
||||
| Goal Lists | `api.goalLists` | Task lists within a goal |
|
||||
| Tasks | `api.tasks` | Full task CRUD, history, assignments |
|
||||
| Tags | `api.tags` | Tag management and task tagging |
|
||||
| Kanban | `api.kanban` | Columns, task ordering, pagination |
|
||||
| Graph | `api.graph` | Task dependency edges |
|
||||
| Collaboration | `api.collaboration` | Users, roles, permissions |
|
||||
|
||||
---
|
||||
|
||||
### Goals
|
||||
|
||||
```typescript
|
||||
// Fetch all goals
|
||||
const goals = await api.goals.fetchGoals();
|
||||
|
||||
// Create a goal
|
||||
const goal = await api.goals.createGoal({
|
||||
name: 'Sprint 1',
|
||||
description: 'First sprint tasks',
|
||||
color: '#4A90D9',
|
||||
});
|
||||
|
||||
// Update a goal
|
||||
await api.goals.updateGoal({
|
||||
id: goal.id,
|
||||
name: 'Sprint 1 (updated)',
|
||||
archive: 0,
|
||||
});
|
||||
|
||||
// Delete a goal
|
||||
await api.goals.deleteGoal(goal.id);
|
||||
```
|
||||
|
||||
### Goal Lists
|
||||
|
||||
Goal lists are task lists that belong to a goal.
|
||||
|
||||
```typescript
|
||||
// Fetch lists for a goal
|
||||
const lists = await api.goalLists.fetchLists({ goalId: goal.id });
|
||||
|
||||
// Create a list
|
||||
const list = await api.goalLists.createList({
|
||||
goalId: goal.id,
|
||||
name: 'Backlog',
|
||||
description: 'Upcoming work',
|
||||
});
|
||||
|
||||
// Update a list
|
||||
await api.goalLists.updateList({
|
||||
id: list.id,
|
||||
name: 'Backlog (v2)',
|
||||
});
|
||||
|
||||
// Delete a list
|
||||
await api.goalLists.deleteList(list.id);
|
||||
```
|
||||
|
||||
### Tasks
|
||||
|
||||
```typescript
|
||||
// Fetch tasks with pagination and filters
|
||||
const tasks = await api.tasks.fetch({
|
||||
goalId: goal.id,
|
||||
componentId: list.id, // list ID, or -1401 for all tasks
|
||||
page: 1,
|
||||
showCompleted: 0, // 0 = hide completed, 1 = show
|
||||
firstNew: 0, // 0 = oldest first, 1 = newest first
|
||||
searchText: 'bug', // optional text search
|
||||
filters: { // optional filters
|
||||
selectedUser: 5,
|
||||
priority: 1,
|
||||
selectedTags: { '12': true, '15': true },
|
||||
},
|
||||
});
|
||||
|
||||
// Create a task
|
||||
const task = await api.tasks.createTask({
|
||||
goalId: goal.id,
|
||||
description: 'Fix login bug',
|
||||
priorityId: 1, // 1 = low, 2 = medium, 3 = high
|
||||
goalListId: list.id,
|
||||
note: 'Details here',
|
||||
startDate: '2025-03-01',
|
||||
endDate: '2025-03-05',
|
||||
});
|
||||
|
||||
// Update a task
|
||||
await api.tasks.updateTask({
|
||||
id: task.id,
|
||||
description: 'Fix login bug (critical)',
|
||||
complete: true,
|
||||
priorityId: 3,
|
||||
});
|
||||
|
||||
// Delete a task
|
||||
await api.tasks.deleteTask(task.id);
|
||||
|
||||
// Fetch a single task by ID
|
||||
const single = await api.tasks.fetchTaskById(task.id);
|
||||
|
||||
// Toggle user assignment
|
||||
await api.tasks.toggleTasksAssignee({
|
||||
taskId: task.id,
|
||||
userId: 42,
|
||||
});
|
||||
|
||||
// Task history
|
||||
const history = await api.tasks.fetchTaskHistory(task.id);
|
||||
await api.tasks.recoveryTaskHistory(history.history[0].historyId, task.id);
|
||||
```
|
||||
|
||||
### Tags
|
||||
|
||||
```typescript
|
||||
// Fetch all tags
|
||||
const tags = await api.tags.fetchAllTagsForUser();
|
||||
|
||||
// Create a tag
|
||||
const tag = await api.tags.createTag({
|
||||
name: 'urgent',
|
||||
color: '#FF0000',
|
||||
goalId: goal.id,
|
||||
});
|
||||
|
||||
// Toggle tag on a task (adds if missing, removes if present)
|
||||
await api.tags.toggleTag({ tagId: tag.id, taskId: task.id });
|
||||
|
||||
// Update a tag
|
||||
await api.tags.updateTag({ id: tag.id, name: 'critical', color: '#CC0000' });
|
||||
|
||||
// Delete a tag
|
||||
await api.tags.deleteTag({ tagId: tag.id });
|
||||
```
|
||||
|
||||
### Kanban
|
||||
|
||||
```typescript
|
||||
// Fetch all columns for a goal
|
||||
const columns = await api.kanban.fetchAllColumns(goal.id);
|
||||
|
||||
// Add a column
|
||||
const column = await api.kanban.addColumn({
|
||||
goalId: goal.id,
|
||||
name: 'In Progress',
|
||||
});
|
||||
|
||||
// Fetch tasks for a column (cursor-based pagination)
|
||||
const result = await api.kanban.fetchTasksForColumn(goal.id, column.id, null);
|
||||
// result.tasks, result.nextCursor, result.columnVersion
|
||||
|
||||
// Move a task between columns / reorder
|
||||
await api.kanban.updateTasksOrderAndColumn({
|
||||
goalId: goal.id,
|
||||
columnId: column.id,
|
||||
taskId: task.id,
|
||||
prevTaskId: null,
|
||||
nextTaskId: 10,
|
||||
});
|
||||
|
||||
// Update a column
|
||||
await api.kanban.updateColumn({ id: column.id, goalId: goal.id, name: 'Review' });
|
||||
|
||||
// Delete a column
|
||||
await api.kanban.deleteColumn({ id: column.id, goalId: goal.id });
|
||||
```
|
||||
|
||||
### Graph (Task Dependencies)
|
||||
|
||||
```typescript
|
||||
// Add a dependency edge (source -> target)
|
||||
const edge = await api.graph.addEdge({ source: 1, target: 2 });
|
||||
|
||||
// Fetch all edges for a goal
|
||||
const edges = await api.graph.fetchAllEdges(goal.id);
|
||||
|
||||
// Delete an edge
|
||||
await api.graph.deleteEdge(edge.id);
|
||||
```
|
||||
|
||||
### Collaboration
|
||||
|
||||
```typescript
|
||||
// Invite a user by email
|
||||
await api.collaboration.inviteUserToGoal({
|
||||
goalId: goal.id,
|
||||
email: 'user@example.com',
|
||||
});
|
||||
|
||||
// Fetch goal members
|
||||
const members = await api.collaboration.fetchUsersForGoal(goal.id);
|
||||
|
||||
// Remove a user
|
||||
await api.collaboration.deleteUserFromGoal({
|
||||
goalId: goal.id,
|
||||
userId: 5,
|
||||
});
|
||||
|
||||
// Roles
|
||||
const roles = await api.collaboration.fetchRolesForGoal(goal.id);
|
||||
const newRole = await api.collaboration.createRoleForGoal({
|
||||
goalId: goal.id,
|
||||
name: 'Developer',
|
||||
});
|
||||
await api.collaboration.toggleUserRoles({
|
||||
goalId: goal.id,
|
||||
userId: 5,
|
||||
roleId: newRole.id,
|
||||
});
|
||||
|
||||
// Permissions
|
||||
const allPermissions = await api.collaboration.fetchAllPermissions();
|
||||
await api.collaboration.toggleRolePermission({
|
||||
goalId: goal.id,
|
||||
roleId: newRole.id,
|
||||
permissionId: 3,
|
||||
});
|
||||
await api.collaboration.deleteRoleFromGoal({
|
||||
goalId: goal.id,
|
||||
roleId: newRole.id,
|
||||
});
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
||||
The package exports `TvPermissions` — a map of all permission constants:
|
||||
|
||||
```typescript
|
||||
import { TvPermissions } from 'taskview-api';
|
||||
|
||||
TvPermissions.GOAL_CAN_DELETE // 'goal_can_delete'
|
||||
TvPermissions.GOAL_CAN_EDIT // 'goal_can_edit'
|
||||
TvPermissions.GOAL_CAN_MANAGE_USERS // 'goal_can_manage_users'
|
||||
TvPermissions.GOAL_CAN_WATCH_CONTENT // 'goal_can_watch_content'
|
||||
TvPermissions.GOAL_CAN_ADD_TASK_LIST // 'goal_can_add_task_list'
|
||||
|
||||
TvPermissions.COMPONENT_CAN_DELETE // 'component_can_delete'
|
||||
TvPermissions.COMPONENT_CAN_EDIT // 'component_can_edit'
|
||||
TvPermissions.COMPONENT_CAN_WATCH_CONTENT // 'component_can_watch_content'
|
||||
TvPermissions.COMPONENT_CAN_ADD_TASKS // 'component_can_add_tasks'
|
||||
|
||||
TvPermissions.TASK_CAN_DELETE // 'task_can_delete'
|
||||
TvPermissions.TASK_CAN_EDIT_DESCRIPTION // 'task_can_edit_description'
|
||||
TvPermissions.TASK_CAN_EDIT_STATUS // 'task_can_edit_status'
|
||||
TvPermissions.TASK_CAN_EDIT_NOTE // 'task_can_edit_note'
|
||||
TvPermissions.TASK_CAN_EDIT_DEADLINE // 'task_can_edit_deadline'
|
||||
TvPermissions.TASK_CAN_EDIT_TAGS // 'task_can_edit_tags'
|
||||
TvPermissions.TASK_CAN_EDIT_PRIORITY // 'task_can_edit_priority'
|
||||
TvPermissions.TASK_CAN_ASSIGN_USERS // 'task_can_assign_users'
|
||||
TvPermissions.TASK_CAN_ACCESS_HISTORY // 'task_can_access_history'
|
||||
// ... and more
|
||||
|
||||
TvPermissions.KANBAN_CAN_MANAGE // 'kanban_can_manage'
|
||||
TvPermissions.KANBAN_CAN_VIEW // 'kanban_can_view'
|
||||
|
||||
TvPermissions.GRAPH_CAN_MANAGE // 'graph_can_manage'
|
||||
TvPermissions.GRAPH_CAN_VIEW // 'graph_can_view'
|
||||
```
|
||||
|
||||
## Build Formats
|
||||
|
||||
The library ships in three formats:
|
||||
|
||||
- **ES Module** — `taskview-api.es.js`
|
||||
- **CommonJS** — `taskview-api.cjs.js`
|
||||
- **UMD** — `taskview-api.umd.js`
|
||||
- **TypeScript declarations** — `index.d.ts`
|
||||
|
||||
## Example
|
||||
|
||||
```typescript
|
||||
import axios from 'axios';
|
||||
import { TvApi } from 'taskview-api';
|
||||
|
||||
const BASE_URL = 'http://localhost:1401';
|
||||
|
||||
const $axios = axios.create({ baseURL: BASE_URL });
|
||||
|
||||
async function login(): Promise<{ access: string; refresh: string }> {
|
||||
const { data } = await $axios.post('/module/auth/login', {
|
||||
login: 'test@mail.dest',
|
||||
password: 'user1!#Q',
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 1. Authenticate
|
||||
console.log('Logging in...');
|
||||
const { access, refresh } = await login();
|
||||
console.log('Logged in. Access token received.');
|
||||
|
||||
// 2. Set auth header and create API instance
|
||||
$axios.defaults.headers.common['Authorization'] = `Bearer ${access}`;
|
||||
const api = new TvApi($axios);
|
||||
|
||||
// 3. Goals
|
||||
console.log('\n--- Goals ---');
|
||||
const goals = await api.goals.fetchGoals();
|
||||
console.log(`Found ${goals.length} goal(s)`);
|
||||
|
||||
const goal = await api.goals.createGoal({
|
||||
name: `Example Goal ${Date.now()}`,
|
||||
description: 'Created by taskview-api example',
|
||||
color: '#4A90D9',
|
||||
});
|
||||
console.log(`Created goal: id=${goal!.id}, name="${goal!.name}"`);
|
||||
|
||||
// 4. Goal Lists
|
||||
console.log('\n--- Goal Lists ---');
|
||||
const list = await api.goalLists.createList({
|
||||
goalId: goal!.id,
|
||||
name: 'Backlog',
|
||||
description: 'Example task list',
|
||||
});
|
||||
console.log(`Created list: id=${list!.id}, name="${list!.name}"`);
|
||||
|
||||
const lists = await api.goalLists.fetchLists({ goalId: goal!.id });
|
||||
console.log(`Goal has ${lists.length} list(s)`);
|
||||
|
||||
// 5. Tasks
|
||||
console.log('\n--- Tasks ---');
|
||||
const task1 = await api.tasks.createTask({
|
||||
goalId: goal!.id,
|
||||
description: 'First task',
|
||||
priorityId: 1,
|
||||
goalListId: list!.id,
|
||||
});
|
||||
console.log(`Created task: id=${task1!.id}, "${task1!.description}"`);
|
||||
|
||||
const task2 = await api.tasks.createTask({
|
||||
goalId: goal!.id,
|
||||
description: 'Second task (high priority)',
|
||||
priorityId: 3,
|
||||
goalListId: list!.id,
|
||||
note: 'This is an important task',
|
||||
});
|
||||
console.log(`Created task: id=${task2!.id}, "${task2!.description}"`);
|
||||
|
||||
const tasks = await api.tasks.fetch({
|
||||
goalId: goal!.id,
|
||||
componentId: list!.id,
|
||||
page: 1,
|
||||
showCompleted: 0,
|
||||
firstNew: 0,
|
||||
});
|
||||
console.log(`Fetched ${tasks.length} task(s) from list`);
|
||||
|
||||
// 6. Update a task
|
||||
await api.tasks.updateTask({
|
||||
id: task1!.id,
|
||||
description: 'First task (updated)',
|
||||
complete: true,
|
||||
});
|
||||
console.log(`Marked task ${task1!.id} as complete`);
|
||||
|
||||
// 7. Tags
|
||||
console.log('\n--- Tags ---');
|
||||
const tag = await api.tags.createTag({
|
||||
name: 'example-tag',
|
||||
color: '#FF5733',
|
||||
goalId: goal!.id,
|
||||
});
|
||||
console.log(`Created tag: id=${tag!.id}, name="${tag!.name}"`);
|
||||
|
||||
await api.tags.toggleTag({ tagId: tag!.id, taskId: task2!.id });
|
||||
console.log(`Added tag "${tag!.name}" to task ${task2!.id}`);
|
||||
|
||||
// 8. Kanban
|
||||
console.log('\n--- Kanban ---');
|
||||
const column = await api.kanban.addColumn({
|
||||
goalId: goal!.id,
|
||||
name: 'In Progress',
|
||||
});
|
||||
console.log(`Created kanban column: id=${column.id}, name="${column.name}"`);
|
||||
|
||||
const columns = await api.kanban.fetchAllColumns(goal!.id);
|
||||
console.log(`Goal has ${columns.length} kanban column(s)`);
|
||||
|
||||
// 9. Graph (task dependencies)
|
||||
console.log('\n--- Graph ---');
|
||||
const edge = await api.graph.addEdge({
|
||||
source: task1!.id,
|
||||
target: task2!.id,
|
||||
});
|
||||
console.log(`Created dependency: task ${edge.fromTaskId} → task ${edge.toTaskId}`);
|
||||
|
||||
const edges = await api.graph.fetchAllEdges(goal!.id);
|
||||
console.log(`Goal has ${edges.length} dependency edge(s)`);
|
||||
|
||||
// 10. Cleanup
|
||||
console.log('\n--- Cleanup ---');
|
||||
await api.graph.deleteEdge(edge.id);
|
||||
console.log('Deleted dependency edge');
|
||||
|
||||
await api.kanban.deleteColumn({ id: column.id, goalId: goal!.id });
|
||||
console.log('Deleted kanban column');
|
||||
|
||||
await api.tags.deleteTag({ tagId: tag!.id });
|
||||
console.log('Deleted tag');
|
||||
|
||||
await api.tasks.deleteTask(task2!.id);
|
||||
await api.tasks.deleteTask(task1!.id);
|
||||
console.log('Deleted tasks');
|
||||
|
||||
await api.goalLists.deleteList(list!.id);
|
||||
console.log('Deleted list');
|
||||
|
||||
await api.goals.deleteGoal(goal!.id);
|
||||
console.log('Deleted goal');
|
||||
|
||||
console.log('\nDone! All examples completed successfully.');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Error:', err.response?.data || err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
```
|
||||
@@ -0,0 +1,147 @@
|
||||
import axios from 'axios';
|
||||
import { TvApi } from 'taskview-api';
|
||||
|
||||
const BASE_URL = 'http://localhost:1401';
|
||||
|
||||
const $axios = axios.create({ baseURL: BASE_URL });
|
||||
|
||||
async function login(): Promise<{ access: string; refresh: string }> {
|
||||
const { data } = await $axios.post('/module/auth/login', {
|
||||
login: 'test@mail.dest',
|
||||
password: 'user1!#Q',
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 1. Authenticate
|
||||
console.log('Logging in...');
|
||||
const { access, refresh } = await login();
|
||||
console.log('Logged in. Access token received.');
|
||||
|
||||
// 2. Set auth header and create API instance
|
||||
$axios.defaults.headers.common['Authorization'] = `Bearer ${access}`;
|
||||
const api = new TvApi($axios);
|
||||
|
||||
// 3. Goals
|
||||
console.log('\n--- Goals ---');
|
||||
const goals = await api.goals.fetchGoals();
|
||||
console.log(`Found ${goals.length} goal(s)`);
|
||||
|
||||
const goal = await api.goals.createGoal({
|
||||
name: `Example Goal ${Date.now()}`,
|
||||
description: 'Created by taskview-api example',
|
||||
color: '#4A90D9',
|
||||
});
|
||||
console.log(`Created goal: id=${goal!.id}, name="${goal!.name}"`);
|
||||
|
||||
// 4. Goal Lists
|
||||
console.log('\n--- Goal Lists ---');
|
||||
const list = await api.goalLists.createList({
|
||||
goalId: goal!.id,
|
||||
name: 'Backlog',
|
||||
description: 'Example task list',
|
||||
});
|
||||
console.log(`Created list: id=${list!.id}, name="${list!.name}"`);
|
||||
|
||||
const lists = await api.goalLists.fetchLists({ goalId: goal!.id });
|
||||
console.log(`Goal has ${lists.length} list(s)`);
|
||||
|
||||
// 5. Tasks
|
||||
console.log('\n--- Tasks ---');
|
||||
const task1 = await api.tasks.createTask({
|
||||
goalId: goal!.id,
|
||||
description: 'First task',
|
||||
priorityId: 1,
|
||||
goalListId: list!.id,
|
||||
});
|
||||
console.log(`Created task: id=${task1!.id}, "${task1!.description}"`);
|
||||
|
||||
const task2 = await api.tasks.createTask({
|
||||
goalId: goal!.id,
|
||||
description: 'Second task (high priority)',
|
||||
priorityId: 3,
|
||||
goalListId: list!.id,
|
||||
note: 'This is an important task',
|
||||
});
|
||||
console.log(`Created task: id=${task2!.id}, "${task2!.description}"`);
|
||||
|
||||
const tasks = await api.tasks.fetch({
|
||||
goalId: goal!.id,
|
||||
componentId: list!.id,
|
||||
page: 1,
|
||||
showCompleted: 0,
|
||||
firstNew: 0,
|
||||
});
|
||||
console.log(`Fetched ${tasks.length} task(s) from list`);
|
||||
|
||||
// 6. Update a task
|
||||
await api.tasks.updateTask({
|
||||
id: task1!.id,
|
||||
description: 'First task (updated)',
|
||||
complete: true,
|
||||
});
|
||||
console.log(`Marked task ${task1!.id} as complete`);
|
||||
|
||||
// 7. Tags
|
||||
console.log('\n--- Tags ---');
|
||||
const tag = await api.tags.createTag({
|
||||
name: 'example-tag',
|
||||
color: '#FF5733',
|
||||
goalId: goal!.id,
|
||||
});
|
||||
console.log(`Created tag: id=${tag!.id}, name="${tag!.name}"`);
|
||||
|
||||
await api.tags.toggleTag({ tagId: tag!.id, taskId: task2!.id });
|
||||
console.log(`Added tag "${tag!.name}" to task ${task2!.id}`);
|
||||
|
||||
// 8. Kanban
|
||||
console.log('\n--- Kanban ---');
|
||||
const column = await api.kanban.addColumn({
|
||||
goalId: goal!.id,
|
||||
name: 'In Progress',
|
||||
});
|
||||
console.log(`Created kanban column: id=${column.id}, name="${column.name}"`);
|
||||
|
||||
const columns = await api.kanban.fetchAllColumns(goal!.id);
|
||||
console.log(`Goal has ${columns.length} kanban column(s)`);
|
||||
|
||||
// 9. Graph (task dependencies)
|
||||
console.log('\n--- Graph ---');
|
||||
const edge = await api.graph.addEdge({
|
||||
source: task1!.id,
|
||||
target: task2!.id,
|
||||
});
|
||||
console.log(`Created dependency: task ${edge.fromTaskId} → task ${edge.toTaskId}`);
|
||||
|
||||
const edges = await api.graph.fetchAllEdges(goal!.id);
|
||||
console.log(`Goal has ${edges.length} dependency edge(s)`);
|
||||
|
||||
// 10. Cleanup
|
||||
console.log('\n--- Cleanup ---');
|
||||
await api.graph.deleteEdge(edge.id);
|
||||
console.log('Deleted dependency edge');
|
||||
|
||||
await api.kanban.deleteColumn({ id: column.id, goalId: goal!.id });
|
||||
console.log('Deleted kanban column');
|
||||
|
||||
await api.tags.deleteTag({ tagId: tag!.id });
|
||||
console.log('Deleted tag');
|
||||
|
||||
await api.tasks.deleteTask(task2!.id);
|
||||
await api.tasks.deleteTask(task1!.id);
|
||||
console.log('Deleted tasks');
|
||||
|
||||
await api.goalLists.deleteList(list!.id);
|
||||
console.log('Deleted list');
|
||||
|
||||
await api.goals.deleteGoal(goal!.id);
|
||||
console.log('Deleted goal');
|
||||
|
||||
console.log('\nDone! All examples completed successfully.');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Error:', err.response?.data || err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
+1786
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "taskview-api-example",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "npx tsx index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"taskview-api": "latest",
|
||||
"axios": "^1.2.3",
|
||||
"tsx": "^4.19.0"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "taskview-api",
|
||||
"private": false,
|
||||
"version": "1.17.0",
|
||||
"version": "1.17.2",
|
||||
"type": "module",
|
||||
"main": "./dist/taskview-api.umd.js",
|
||||
"module": "./dist/taskview-api.es.js",
|
||||
|
||||
+2
-1
@@ -35,4 +35,5 @@ Info.plist
|
||||
e2e_logs
|
||||
e2e_pgdata
|
||||
e2e_updates
|
||||
playwright-report
|
||||
playwright-report
|
||||
test-results
|
||||
@@ -1,29 +1,27 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { TEST_USER } from './fixtures/auth'
|
||||
import { setEnglishLocale } from './test-helpers'
|
||||
|
||||
test.describe('Login', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await setEnglishLocale(page)
|
||||
await page.reload()
|
||||
})
|
||||
|
||||
test('shows login page with welcome message', async ({ page }) => {
|
||||
await expect(page.getByRole('heading', { level: 1 })).toContainText(/welcome|возвращением/i)
|
||||
await expect(page.getByRole('heading', { level: 1 })).toContainText(/welcome/i)
|
||||
})
|
||||
|
||||
test('user can login with password and reach dashboard', async ({ page }) => {
|
||||
// Switch to Password tab (default is Magic link)
|
||||
await page.getByRole('tab', { name: /password|пароль/i }).click()
|
||||
await page.getByRole('tab', { name: /password/i }).click()
|
||||
|
||||
// Fill credentials
|
||||
await page.getByTestId('login-input').fill(TEST_USER.login)
|
||||
await page.getByTestId('password-input').fill(TEST_USER.password)
|
||||
|
||||
// Submit
|
||||
await page.getByTestId('sign-in-button').click()
|
||||
|
||||
// Should redirect to user dashboard
|
||||
await expect(page).toHaveURL(/\/user/)
|
||||
// Dashboard should be visible (sidebar or main content)
|
||||
await expect(page.locator('body')).not.toContainText(/invalid|неверный|error|ошибка/i)
|
||||
await expect(page.locator('body')).not.toContainText(/invalid|error/i)
|
||||
})
|
||||
})
|
||||
|
||||
+20
-21
@@ -1,26 +1,22 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { login } from './test-helpers'
|
||||
|
||||
async function addProject(page: import('@playwright/test').Page, name: string) {
|
||||
const input = page.getByTestId('project-add-input').first()
|
||||
await input.waitFor({ state: 'visible', timeout: 15000 })
|
||||
await input.fill(name)
|
||||
await input.press('Enter')
|
||||
await expect(page.getByText(name).first()).toBeVisible({ timeout: 10000 })
|
||||
}
|
||||
|
||||
async function openProjectMenu(page: import('@playwright/test').Page, projectName: string) {
|
||||
const row = page.getByTestId(`project-row-${projectName}`)
|
||||
await row.first().waitFor({ state: 'visible', timeout: 10000 })
|
||||
await row.first().getByTestId('project-menu-trigger').click()
|
||||
}
|
||||
import {
|
||||
setupAndLogin,
|
||||
addProject,
|
||||
openProjectMenu,
|
||||
cleanupProjects,
|
||||
} from './test-helpers'
|
||||
|
||||
test.describe('Projects', () => {
|
||||
test.setTimeout(15_000)
|
||||
test.setTimeout(30_000)
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await login(page)
|
||||
await setupAndLogin(page)
|
||||
})
|
||||
|
||||
test.afterAll(async ({ browser }) => {
|
||||
const page = await browser.newPage()
|
||||
await cleanupProjects(page)
|
||||
await page.close()
|
||||
})
|
||||
|
||||
test('user can add project', async ({ page }) => {
|
||||
@@ -36,16 +32,17 @@ test.describe('Projects', () => {
|
||||
await page.getByTestId('context-menu-edit').click()
|
||||
await page.getByTestId('project-edit-name').fill(newName)
|
||||
await page.getByTestId('project-edit-save').click()
|
||||
|
||||
|
||||
await expect(page.getByText(newName).first()).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
|
||||
test('user can delete project', async ({ page }) => {
|
||||
const projectName = `Delete Me ${Date.now()}`
|
||||
await addProject(page, projectName)
|
||||
await page.waitForTimeout(1000)
|
||||
await openProjectMenu(page, projectName)
|
||||
await page.getByTestId('context-menu-delete').click()
|
||||
await page.getByRole('button', { name: /^yes|^да$/i }).click()
|
||||
await page.getByTestId('context-menu-delete').click({ force: true })
|
||||
await page.getByTestId('confirm-delete-button').click()
|
||||
await expect(page.getByText(projectName)).toHaveCount(0)
|
||||
})
|
||||
|
||||
@@ -63,7 +60,9 @@ test.describe('Projects', () => {
|
||||
await addProject(page, projectName)
|
||||
await openProjectMenu(page, projectName)
|
||||
await page.getByTestId('context-menu-move-to-archive').click()
|
||||
await page.waitForTimeout(1000)
|
||||
await page.getByTestId('archive-list-collapsible').click()
|
||||
await page.getByTestId(`project-row-${projectName}`).first().waitFor({ state: 'visible', timeout: 5000 })
|
||||
await openProjectMenu(page, projectName)
|
||||
await page.getByTestId('context-menu-restore-from-archive').click({ force: true })
|
||||
await expect(page.getByText(projectName).first()).toBeVisible({ timeout: 5000 })
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import {
|
||||
setupAndLogin,
|
||||
createProjectAndNavigate,
|
||||
cleanupProjects,
|
||||
addTaskFromList,
|
||||
openTaskDetail,
|
||||
addSubtask,
|
||||
getSubtaskCount,
|
||||
navigateToKanban,
|
||||
navigateToGraph,
|
||||
} from './test-helpers'
|
||||
|
||||
test.describe('Subtask duplication', () => {
|
||||
test.setTimeout(60_000)
|
||||
|
||||
let projectName: string
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupAndLogin(page)
|
||||
})
|
||||
|
||||
test.afterAll(async ({ browser }) => {
|
||||
const page = await browser.newPage()
|
||||
await cleanupProjects(page)
|
||||
await page.close()
|
||||
})
|
||||
|
||||
test('adding subtask from task list does not duplicate', async ({ page }) => {
|
||||
const project = await createProjectAndNavigate(page)
|
||||
projectName = project.name
|
||||
|
||||
const taskName = `List Task ${Date.now()}`
|
||||
await addTaskFromList(page, taskName)
|
||||
await openTaskDetail(page, taskName)
|
||||
|
||||
const count = await addSubtask(page)
|
||||
expect(count).toBe(1)
|
||||
})
|
||||
|
||||
test('adding subtask from kanban does not duplicate', async ({ page }) => {
|
||||
const project = await createProjectAndNavigate(page)
|
||||
projectName = project.name
|
||||
|
||||
const taskName = `Kanban Task ${Date.now()}`
|
||||
await addTaskFromList(page, taskName)
|
||||
|
||||
await navigateToKanban(page, projectName)
|
||||
|
||||
await expect(page.getByText(taskName).first()).toBeVisible({ timeout: 10000 })
|
||||
await openTaskDetail(page, taskName)
|
||||
|
||||
const count = await addSubtask(page)
|
||||
expect(count).toBe(1)
|
||||
})
|
||||
|
||||
test('adding subtask from graph does not duplicate', async ({ page }) => {
|
||||
const project = await createProjectAndNavigate(page)
|
||||
projectName = project.name
|
||||
|
||||
const taskName = `Graph Task ${Date.now()}`
|
||||
await addTaskFromList(page, taskName)
|
||||
|
||||
await navigateToGraph(page, projectName)
|
||||
|
||||
await expect(page.getByText(taskName).first()).toBeVisible({ timeout: 10000 })
|
||||
await openTaskDetail(page, taskName)
|
||||
|
||||
const count = await addSubtask(page)
|
||||
expect(count).toBe(1)
|
||||
})
|
||||
|
||||
test('adding multiple subtasks does not duplicate any of them', async ({ page }) => {
|
||||
const project = await createProjectAndNavigate(page)
|
||||
projectName = project.name
|
||||
|
||||
const taskName = `Multi Subtask ${Date.now()}`
|
||||
await addTaskFromList(page, taskName)
|
||||
await openTaskDetail(page, taskName)
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await addSubtask(page)
|
||||
}
|
||||
|
||||
const count = await getSubtaskCount(page)
|
||||
expect(count).toBe(3)
|
||||
})
|
||||
})
|
||||
+154
-2
@@ -1,8 +1,160 @@
|
||||
import { expect, type Page } from '@playwright/test'
|
||||
import { TEST_USER } from './fixtures/auth'
|
||||
|
||||
const API_URL = 'http://localhost:1401'
|
||||
|
||||
// ── Cleanup ──
|
||||
|
||||
const createdProjectIds: number[] = []
|
||||
|
||||
export function trackProjectId(id: number | null) {
|
||||
if (id) createdProjectIds.push(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all tracked projects via API. Call in afterAll.
|
||||
* Authenticates via API login to get a JWT token (page.request alone has no Bearer header).
|
||||
*/
|
||||
export async function cleanupProjects(page: Page) {
|
||||
if (createdProjectIds.length === 0) return
|
||||
|
||||
const loginResponse = await page.request.post(`${API_URL}/module/auth/login`, {
|
||||
form: { login: TEST_USER.login, password: TEST_USER.password },
|
||||
})
|
||||
const loginData = await loginResponse.json()
|
||||
const token = loginData.access
|
||||
if (!token) {
|
||||
console.error('Cleanup: failed to get auth token, skipping project deletion')
|
||||
return
|
||||
}
|
||||
|
||||
for (const id of createdProjectIds) {
|
||||
try {
|
||||
await page.request.delete(`${API_URL}/module/goals`, {
|
||||
data: { goalId: id },
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
timeout: 5000,
|
||||
})
|
||||
} catch {
|
||||
console.error(`Failed to delete project ${id} via API`)
|
||||
}
|
||||
}
|
||||
createdProjectIds.length = 0
|
||||
}
|
||||
|
||||
// ── Locale ──
|
||||
|
||||
/**
|
||||
* Sets the app locale to English via localStorage before page load.
|
||||
* Must be called after page.goto() so localStorage is available for the domain,
|
||||
* then reload to apply.
|
||||
*/
|
||||
export async function setEnglishLocale(page: Page) {
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('store_task_view.task_view.locale', 'en')
|
||||
})
|
||||
}
|
||||
|
||||
// ── Auth ──
|
||||
|
||||
export async function login(page: Page) {
|
||||
await page.getByRole('tab', { name: /password|пароль/i }).click()
|
||||
await page.getByRole('tab', { name: /password/i }).click()
|
||||
await page.getByTestId('login-input').fill(TEST_USER.login)
|
||||
await page.getByTestId('password-input').fill(TEST_USER.password)
|
||||
await page.getByTestId('sign-in-button').click()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to `/`, sets English locale, reloads, and logs in.
|
||||
*/
|
||||
export async function setupAndLogin(page: Page) {
|
||||
await page.goto('/')
|
||||
await setEnglishLocale(page)
|
||||
await page.reload()
|
||||
await login(page)
|
||||
}
|
||||
|
||||
// ── Projects ──
|
||||
|
||||
export async function addProject(page: Page, name: string): Promise<number | null> {
|
||||
const urlBefore = page.url()
|
||||
const input = page.getByTestId('project-add-input').first()
|
||||
await input.waitFor({ state: 'visible', timeout: 15000 })
|
||||
await input.fill(name)
|
||||
await input.press('Enter')
|
||||
await expect(page.getByText(name).first()).toBeVisible({ timeout: 10000 })
|
||||
await page.waitForFunction(
|
||||
(prev) => location.href !== prev && /\/user\/\d+\//.test(location.href),
|
||||
urlBefore,
|
||||
{ timeout: 10000 },
|
||||
)
|
||||
const id = extractProjectId(page.url())
|
||||
trackProjectId(id)
|
||||
return id
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a project, waits for auto-navigation, and tracks the ID for cleanup.
|
||||
*/
|
||||
export async function createProjectAndNavigate(page: Page, prefix = 'Test') {
|
||||
const projectName = `${prefix} ${Date.now()}`
|
||||
const id = await addProject(page, projectName)
|
||||
await expect(page.getByTestId('task-search-add-input')).toBeVisible({ timeout: 10000 })
|
||||
return { name: projectName, id }
|
||||
}
|
||||
|
||||
export function extractProjectId(url: string): number | null {
|
||||
const match = url.match(/\/user\/(\d+)\//)
|
||||
return match ? Number(match[1]) : null
|
||||
}
|
||||
|
||||
export async function openProjectMenu(page: Page, projectName: string) {
|
||||
const row = page.getByTestId(`project-row-${projectName}`)
|
||||
await row.first().waitFor({ state: 'visible', timeout: 10000 })
|
||||
await row.first().getByTestId('project-menu-trigger').click({ force: true })
|
||||
}
|
||||
|
||||
// ── Navigation ──
|
||||
|
||||
export async function navigateToKanban(page: Page, projectName: string) {
|
||||
await openProjectMenu(page, projectName)
|
||||
await page.getByRole('link', { name: /kanban/i }).click()
|
||||
await page.waitForURL(/\/kanban/, { timeout: 10000 })
|
||||
}
|
||||
|
||||
export async function navigateToGraph(page: Page, projectName: string) {
|
||||
await openProjectMenu(page, projectName)
|
||||
await page.getByRole('link', { name: /graph/i }).click()
|
||||
await page.waitForURL(/\/graph/, { timeout: 10000 })
|
||||
}
|
||||
|
||||
// ── Tasks ──
|
||||
|
||||
export async function addTaskFromList(page: Page, taskName: string) {
|
||||
const input = page.getByTestId('task-search-add-input')
|
||||
await input.waitFor({ state: 'visible', timeout: 10000 })
|
||||
await input.fill(taskName)
|
||||
await input.press('Enter')
|
||||
await expect(page.getByText(taskName).first()).toBeVisible({ timeout: 10000 })
|
||||
}
|
||||
|
||||
export async function openTaskDetail(page: Page, taskName: string) {
|
||||
await page.getByText(taskName).first().click()
|
||||
await expect(page.getByTestId('add-subtask-button')).toBeVisible({ timeout: 10000 })
|
||||
}
|
||||
|
||||
// ── Subtasks ──
|
||||
|
||||
/**
|
||||
* Clicks "Add subtask" and returns the total count of subtask items.
|
||||
*/
|
||||
export async function addSubtask(page: Page) {
|
||||
await page.getByTestId('add-subtask-button').click()
|
||||
await expect(page.getByTestId('subtasks-list')).toBeVisible({ timeout: 10000 })
|
||||
await page.waitForTimeout(500)
|
||||
return page.locator('[data-testid^="subtask-item-"]').count()
|
||||
}
|
||||
|
||||
export function getSubtaskCount(page: Page) {
|
||||
return page.locator('[data-testid^="subtask-item-"]').count()
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "web-nuxt-ui",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "1.20.4",
|
||||
"version": "1.20.7",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "pnpm run typecheck && vite build",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
v-model="taskName"
|
||||
:loading="loading"
|
||||
:placeholder="t('kanban.addTask')"
|
||||
data-testid="kanban-add-task-input"
|
||||
:trailing-icon="inputIcon"
|
||||
icon="i-lucide-plus"
|
||||
size="xl"
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
color="error"
|
||||
class="min-w-12"
|
||||
:ui="{base: 'justify-center'}"
|
||||
data-testid="confirm-delete-button"
|
||||
@click="confirm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col gap-3 p-3 rounded-lg border border-default hover:bg-elevated transition-colors cursor-pointer"
|
||||
:data-testid="`task-item-${task.id}`"
|
||||
@click="handleOpenTask"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
:loading="loading"
|
||||
variant="soft"
|
||||
class="w-full"
|
||||
data-testid="task-search-add-input"
|
||||
:ui="{
|
||||
base: 'bg-tv-ui-bg-elevated',
|
||||
}"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-2 group ">
|
||||
<div
|
||||
class="flex items-center gap-2 group"
|
||||
:data-testid="`subtask-item-${subtask.id}`"
|
||||
>
|
||||
<UTextarea
|
||||
ref="inputRef"
|
||||
v-model="localDescription"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<div
|
||||
v-if="subtasks.length > 0 && canViewTaskSubtasks"
|
||||
class="space-y-2 mb-2"
|
||||
data-testid="subtasks-list"
|
||||
>
|
||||
<TaskSubtaskItem
|
||||
v-for="subtask in subtasks"
|
||||
@@ -25,6 +26,7 @@
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
class="w-full justify-start rounded-lg shadow-sm dark:bg-tv-ui-bg-elevated"
|
||||
data-testid="add-subtask-button"
|
||||
:loading="isAdding"
|
||||
@click="addSubtask"
|
||||
>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { $ls } from '@/plugins/axios'
|
||||
|
||||
export async function useLogout() {
|
||||
const result = await $api.post<{ logout: boolean }>('/module/auth/logout').catch((err) => {
|
||||
console.log(err, $api)
|
||||
console.error(err, $api)
|
||||
})
|
||||
|
||||
if (result) {
|
||||
|
||||
@@ -113,12 +113,11 @@ export const useTasksStore = defineStore('tasks', {
|
||||
throw new Error('Main task not found for adding subtask')
|
||||
}
|
||||
|
||||
if (mainTask) {
|
||||
mainTask.subtasks.push(task)
|
||||
}
|
||||
mainTask.subtasks.push(task)
|
||||
|
||||
if (this.selectedTask?.id === task.parentId) {
|
||||
this.selectedTask.subtasks.push(task)
|
||||
// Sync selectedTask if it's a separate object for the same parent
|
||||
if (this.selectedTask && this.selectedTask !== mainTask && this.selectedTask.id === task.parentId) {
|
||||
this.selectedTask.subtasks = mainTask.subtasks
|
||||
}
|
||||
} else {
|
||||
this.tasks.unshift(task)
|
||||
|
||||
Reference in New Issue
Block a user