4.19.0: Добавлены позиции подзадач
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m34s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m34s
This commit is contained in:
@@ -284,6 +284,7 @@ type Task struct {
|
|||||||
WishlistID *int `json:"wishlist_id,omitempty"`
|
WishlistID *int `json:"wishlist_id,omitempty"`
|
||||||
ConfigID *int `json:"config_id,omitempty"`
|
ConfigID *int `json:"config_id,omitempty"`
|
||||||
RewardPolicy *string `json:"reward_policy,omitempty"` // "personal" или "general" для задач, связанных с желаниями
|
RewardPolicy *string `json:"reward_policy,omitempty"` // "personal" или "general" для задач, связанных с желаниями
|
||||||
|
Position *int `json:"position,omitempty"` // Position for subtasks
|
||||||
// Дополнительные поля для списка задач (без omitempty чтобы всегда передавались)
|
// Дополнительные поля для списка задач (без omitempty чтобы всегда передавались)
|
||||||
ProjectNames []string `json:"project_names"`
|
ProjectNames []string `json:"project_names"`
|
||||||
SubtasksCount int `json:"subtasks_count"`
|
SubtasksCount int `json:"subtasks_count"`
|
||||||
@@ -335,6 +336,7 @@ type SubtaskRequest struct {
|
|||||||
ID *int `json:"id,omitempty"`
|
ID *int `json:"id,omitempty"`
|
||||||
Name *string `json:"name,omitempty"`
|
Name *string `json:"name,omitempty"`
|
||||||
RewardMessage *string `json:"reward_message,omitempty"`
|
RewardMessage *string `json:"reward_message,omitempty"`
|
||||||
|
Position *int `json:"position,omitempty"`
|
||||||
Rewards []RewardRequest `json:"rewards,omitempty"`
|
Rewards []RewardRequest `json:"rewards,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7370,10 +7372,10 @@ func (a *App) getTaskDetailHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Получаем подзадачи
|
// Получаем подзадачи
|
||||||
subtasks := make([]Subtask, 0)
|
subtasks := make([]Subtask, 0)
|
||||||
subtaskRows, err := a.DB.Query(`
|
subtaskRows, err := a.DB.Query(`
|
||||||
SELECT id, name, completed, last_completed_at, reward_message, progression_base
|
SELECT id, name, completed, last_completed_at, reward_message, progression_base, position
|
||||||
FROM tasks
|
FROM tasks
|
||||||
WHERE parent_task_id = $1 AND deleted = FALSE
|
WHERE parent_task_id = $1 AND deleted = FALSE
|
||||||
ORDER BY id
|
ORDER BY COALESCE(position, id)
|
||||||
`, taskID)
|
`, taskID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -7388,10 +7390,12 @@ func (a *App) getTaskDetailHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
var subtaskRewardMessage sql.NullString
|
var subtaskRewardMessage sql.NullString
|
||||||
var subtaskProgressionBase sql.NullFloat64
|
var subtaskProgressionBase sql.NullFloat64
|
||||||
var subtaskLastCompletedAt sql.NullString
|
var subtaskLastCompletedAt sql.NullString
|
||||||
|
var subtaskPosition sql.NullInt64
|
||||||
|
|
||||||
err := subtaskRows.Scan(
|
err := subtaskRows.Scan(
|
||||||
&subtaskTask.ID, &subtaskTask.Name, &subtaskTask.Completed,
|
&subtaskTask.ID, &subtaskTask.Name, &subtaskTask.Completed,
|
||||||
&subtaskLastCompletedAt, &subtaskRewardMessage, &subtaskProgressionBase,
|
&subtaskLastCompletedAt, &subtaskRewardMessage, &subtaskProgressionBase,
|
||||||
|
&subtaskPosition,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error scanning subtask: %v", err)
|
log.Printf("Error scanning subtask: %v", err)
|
||||||
@@ -7407,6 +7411,10 @@ func (a *App) getTaskDetailHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if subtaskLastCompletedAt.Valid {
|
if subtaskLastCompletedAt.Valid {
|
||||||
subtaskTask.LastCompletedAt = &subtaskLastCompletedAt.String
|
subtaskTask.LastCompletedAt = &subtaskLastCompletedAt.String
|
||||||
}
|
}
|
||||||
|
if subtaskPosition.Valid {
|
||||||
|
pos := int(subtaskPosition.Int64)
|
||||||
|
subtaskTask.Position = &pos
|
||||||
|
}
|
||||||
|
|
||||||
subtaskIDs = append(subtaskIDs, subtaskTask.ID)
|
subtaskIDs = append(subtaskIDs, subtaskTask.ID)
|
||||||
subtask := Subtask{
|
subtask := Subtask{
|
||||||
@@ -7944,10 +7952,11 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Создаем подзадачи
|
// Создаем подзадачи
|
||||||
for _, subtaskReq := range req.Subtasks {
|
for index, subtaskReq := range req.Subtasks {
|
||||||
var subtaskName sql.NullString
|
var subtaskName sql.NullString
|
||||||
var subtaskRewardMessage sql.NullString
|
var subtaskRewardMessage sql.NullString
|
||||||
var subtaskProgressionBase sql.NullFloat64
|
var subtaskProgressionBase sql.NullFloat64
|
||||||
|
var subtaskPosition sql.NullInt64
|
||||||
|
|
||||||
if subtaskReq.Name != nil && strings.TrimSpace(*subtaskReq.Name) != "" {
|
if subtaskReq.Name != nil && strings.TrimSpace(*subtaskReq.Name) != "" {
|
||||||
subtaskName = sql.NullString{String: strings.TrimSpace(*subtaskReq.Name), Valid: true}
|
subtaskName = sql.NullString{String: strings.TrimSpace(*subtaskReq.Name), Valid: true}
|
||||||
@@ -7958,13 +7967,19 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if req.ProgressionBase != nil {
|
if req.ProgressionBase != nil {
|
||||||
subtaskProgressionBase = sql.NullFloat64{Float64: *req.ProgressionBase, Valid: true}
|
subtaskProgressionBase = sql.NullFloat64{Float64: *req.ProgressionBase, Valid: true}
|
||||||
}
|
}
|
||||||
|
// Используем position из запроса, если указан, иначе используем индекс в массиве
|
||||||
|
if subtaskReq.Position != nil {
|
||||||
|
subtaskPosition = sql.NullInt64{Int64: int64(*subtaskReq.Position), Valid: true}
|
||||||
|
} else {
|
||||||
|
subtaskPosition = sql.NullInt64{Int64: int64(index), Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
var subtaskID int
|
var subtaskID int
|
||||||
err = tx.QueryRow(`
|
err = tx.QueryRow(`
|
||||||
INSERT INTO tasks (user_id, name, parent_task_id, reward_message, progression_base, completed, deleted)
|
INSERT INTO tasks (user_id, name, parent_task_id, reward_message, progression_base, completed, deleted, position)
|
||||||
VALUES ($1, $2, $3, $4, $5, 0, FALSE)
|
VALUES ($1, $2, $3, $4, $5, 0, FALSE, $6)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`, userID, subtaskName, taskID, subtaskRewardMessage, subtaskProgressionBase).Scan(&subtaskID)
|
`, userID, subtaskName, taskID, subtaskRewardMessage, subtaskProgressionBase, subtaskPosition).Scan(&subtaskID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error creating subtask: %v", err)
|
log.Printf("Error creating subtask: %v", err)
|
||||||
@@ -8363,7 +8378,7 @@ func (a *App) updateTaskHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Обрабатываем подзадачи из запроса
|
// Обрабатываем подзадачи из запроса
|
||||||
subtaskIDsInRequest := make(map[int]bool)
|
subtaskIDsInRequest := make(map[int]bool)
|
||||||
for _, subtaskReq := range req.Subtasks {
|
for index, subtaskReq := range req.Subtasks {
|
||||||
if subtaskReq.ID != nil {
|
if subtaskReq.ID != nil {
|
||||||
subtaskIDsInRequest[*subtaskReq.ID] = true
|
subtaskIDsInRequest[*subtaskReq.ID] = true
|
||||||
|
|
||||||
@@ -8371,6 +8386,7 @@ func (a *App) updateTaskHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
var subtaskName sql.NullString
|
var subtaskName sql.NullString
|
||||||
var subtaskRewardMessage sql.NullString
|
var subtaskRewardMessage sql.NullString
|
||||||
var subtaskProgressionBase sql.NullFloat64
|
var subtaskProgressionBase sql.NullFloat64
|
||||||
|
var subtaskPosition sql.NullInt64
|
||||||
|
|
||||||
if subtaskReq.Name != nil && strings.TrimSpace(*subtaskReq.Name) != "" {
|
if subtaskReq.Name != nil && strings.TrimSpace(*subtaskReq.Name) != "" {
|
||||||
subtaskName = sql.NullString{String: strings.TrimSpace(*subtaskReq.Name), Valid: true}
|
subtaskName = sql.NullString{String: strings.TrimSpace(*subtaskReq.Name), Valid: true}
|
||||||
@@ -8381,12 +8397,18 @@ func (a *App) updateTaskHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if req.ProgressionBase != nil {
|
if req.ProgressionBase != nil {
|
||||||
subtaskProgressionBase = sql.NullFloat64{Float64: *req.ProgressionBase, Valid: true}
|
subtaskProgressionBase = sql.NullFloat64{Float64: *req.ProgressionBase, Valid: true}
|
||||||
}
|
}
|
||||||
|
// Используем position из запроса, если указан, иначе используем индекс в массиве
|
||||||
|
if subtaskReq.Position != nil {
|
||||||
|
subtaskPosition = sql.NullInt64{Int64: int64(*subtaskReq.Position), Valid: true}
|
||||||
|
} else {
|
||||||
|
subtaskPosition = sql.NullInt64{Int64: int64(index), Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
_, err = tx.Exec(`
|
_, err = tx.Exec(`
|
||||||
UPDATE tasks
|
UPDATE tasks
|
||||||
SET name = $1, reward_message = $2, progression_base = $3
|
SET name = $1, reward_message = $2, progression_base = $3, position = $4
|
||||||
WHERE id = $4 AND parent_task_id = $5
|
WHERE id = $5 AND parent_task_id = $6
|
||||||
`, subtaskName, subtaskRewardMessage, subtaskProgressionBase, *subtaskReq.ID, taskID)
|
`, subtaskName, subtaskRewardMessage, subtaskProgressionBase, subtaskPosition, *subtaskReq.ID, taskID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error updating subtask: %v", err)
|
log.Printf("Error updating subtask: %v", err)
|
||||||
@@ -8432,6 +8454,7 @@ func (a *App) updateTaskHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
var subtaskName sql.NullString
|
var subtaskName sql.NullString
|
||||||
var subtaskRewardMessage sql.NullString
|
var subtaskRewardMessage sql.NullString
|
||||||
var subtaskProgressionBase sql.NullFloat64
|
var subtaskProgressionBase sql.NullFloat64
|
||||||
|
var subtaskPosition sql.NullInt64
|
||||||
|
|
||||||
if subtaskReq.Name != nil && strings.TrimSpace(*subtaskReq.Name) != "" {
|
if subtaskReq.Name != nil && strings.TrimSpace(*subtaskReq.Name) != "" {
|
||||||
subtaskName = sql.NullString{String: strings.TrimSpace(*subtaskReq.Name), Valid: true}
|
subtaskName = sql.NullString{String: strings.TrimSpace(*subtaskReq.Name), Valid: true}
|
||||||
@@ -8442,13 +8465,19 @@ func (a *App) updateTaskHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if req.ProgressionBase != nil {
|
if req.ProgressionBase != nil {
|
||||||
subtaskProgressionBase = sql.NullFloat64{Float64: *req.ProgressionBase, Valid: true}
|
subtaskProgressionBase = sql.NullFloat64{Float64: *req.ProgressionBase, Valid: true}
|
||||||
}
|
}
|
||||||
|
// Используем position из запроса, если указан, иначе используем индекс в массиве
|
||||||
|
if subtaskReq.Position != nil {
|
||||||
|
subtaskPosition = sql.NullInt64{Int64: int64(*subtaskReq.Position), Valid: true}
|
||||||
|
} else {
|
||||||
|
subtaskPosition = sql.NullInt64{Int64: int64(index), Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
var subtaskID int
|
var subtaskID int
|
||||||
err = tx.QueryRow(`
|
err = tx.QueryRow(`
|
||||||
INSERT INTO tasks (user_id, name, parent_task_id, reward_message, progression_base, completed, deleted)
|
INSERT INTO tasks (user_id, name, parent_task_id, reward_message, progression_base, completed, deleted, position)
|
||||||
VALUES ($1, $2, $3, $4, $5, 0, FALSE)
|
VALUES ($1, $2, $3, $4, $5, 0, FALSE, $6)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`, userID, subtaskName, taskID, subtaskRewardMessage, subtaskProgressionBase).Scan(&subtaskID)
|
`, userID, subtaskName, taskID, subtaskRewardMessage, subtaskProgressionBase, subtaskPosition).Scan(&subtaskID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error creating subtask: %v", err)
|
log.Printf("Error creating subtask: %v", err)
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- Migration: Remove position field from tasks table
|
||||||
|
-- Date: 2026-02-02
|
||||||
|
--
|
||||||
|
-- This migration removes the position field from tasks table.
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_tasks_parent_position;
|
||||||
|
|
||||||
|
ALTER TABLE tasks
|
||||||
|
DROP COLUMN IF EXISTS position;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
-- Migration: Add position field to tasks table for subtasks ordering
|
||||||
|
-- Date: 2026-02-02
|
||||||
|
--
|
||||||
|
-- This migration adds position field to tasks table to allow
|
||||||
|
-- custom ordering of subtasks. The field is NULL for regular tasks
|
||||||
|
-- and contains position number for subtasks (tasks with parent_task_id).
|
||||||
|
|
||||||
|
-- Добавляем поле position
|
||||||
|
ALTER TABLE tasks
|
||||||
|
ADD COLUMN position INTEGER;
|
||||||
|
|
||||||
|
-- Заполняем позиции для всех существующих подзадач
|
||||||
|
-- Позиции присваиваются по порядку id в рамках каждой родительской задачи
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
parent_record RECORD;
|
||||||
|
subtask_record RECORD;
|
||||||
|
pos INTEGER;
|
||||||
|
BEGIN
|
||||||
|
-- Для каждой родительской задачи
|
||||||
|
FOR parent_record IN
|
||||||
|
SELECT DISTINCT parent_task_id
|
||||||
|
FROM tasks
|
||||||
|
WHERE parent_task_id IS NOT NULL
|
||||||
|
ORDER BY parent_task_id
|
||||||
|
LOOP
|
||||||
|
pos := 0;
|
||||||
|
-- Обновляем подзадачи этой родительской задачи
|
||||||
|
FOR subtask_record IN
|
||||||
|
SELECT id
|
||||||
|
FROM tasks
|
||||||
|
WHERE parent_task_id = parent_record.parent_task_id
|
||||||
|
AND deleted = FALSE
|
||||||
|
ORDER BY id
|
||||||
|
LOOP
|
||||||
|
UPDATE tasks
|
||||||
|
SET position = pos
|
||||||
|
WHERE id = subtask_record.id;
|
||||||
|
|
||||||
|
pos := pos + 1;
|
||||||
|
END LOOP;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Создаем индекс для быстрой сортировки подзадач
|
||||||
|
CREATE INDEX idx_tasks_parent_position ON tasks(parent_task_id, position)
|
||||||
|
WHERE parent_task_id IS NOT NULL AND deleted = FALSE;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN tasks.position IS 'Position of subtask within parent task. NULL for regular tasks.';
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "play-life-web",
|
"name": "play-life-web",
|
||||||
"version": "4.18.0",
|
"version": "4.19.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -261,6 +261,46 @@
|
|||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subtask-position-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.move-subtask-button {
|
||||||
|
padding: 0.25rem;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #6b7280;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.move-subtask-button:hover:not(:disabled) {
|
||||||
|
background: #e5e7eb;
|
||||||
|
border-color: #9ca3af;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.move-subtask-button:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.move-subtask-button:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.subtask-name-input {
|
.subtask-name-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|||||||
@@ -313,10 +313,11 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
|||||||
// Для задач-тестов не загружаем подзадачи
|
// Для задач-тестов не загружаем подзадачи
|
||||||
setSubtasks([])
|
setSubtasks([])
|
||||||
} else {
|
} else {
|
||||||
setSubtasks(data.subtasks.map(st => ({
|
setSubtasks(data.subtasks.map((st, index) => ({
|
||||||
id: st.task.id,
|
id: st.task.id,
|
||||||
name: st.task.name || '',
|
name: st.task.name || '',
|
||||||
reward_message: st.task.reward_message || '',
|
reward_message: st.task.reward_message || '',
|
||||||
|
position: st.task.position !== undefined && st.task.position !== null ? st.task.position : index,
|
||||||
rewards: st.rewards.map(r => ({
|
rewards: st.rewards.map(r => ({
|
||||||
position: r.position,
|
position: r.position,
|
||||||
project_name: r.project_name,
|
project_name: r.project_name,
|
||||||
@@ -483,6 +484,7 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
|||||||
id: null,
|
id: null,
|
||||||
name: '',
|
name: '',
|
||||||
reward_message: '',
|
reward_message: '',
|
||||||
|
position: subtasks.length,
|
||||||
rewards: []
|
rewards: []
|
||||||
}])
|
}])
|
||||||
}
|
}
|
||||||
@@ -520,7 +522,38 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleRemoveSubtask = (index) => {
|
const handleRemoveSubtask = (index) => {
|
||||||
setSubtasks(subtasks.filter((_, i) => i !== index))
|
const newSubtasks = subtasks.filter((_, i) => i !== index)
|
||||||
|
// Пересчитываем позиции после удаления
|
||||||
|
newSubtasks.forEach((st, i) => {
|
||||||
|
st.position = i
|
||||||
|
})
|
||||||
|
setSubtasks(newSubtasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMoveSubtaskUp = (index) => {
|
||||||
|
if (index === 0) return // Нельзя переместить первый элемент вверх
|
||||||
|
const newSubtasks = [...subtasks]
|
||||||
|
const temp = newSubtasks[index]
|
||||||
|
newSubtasks[index] = newSubtasks[index - 1]
|
||||||
|
newSubtasks[index - 1] = temp
|
||||||
|
// Обновляем позиции
|
||||||
|
newSubtasks.forEach((st, i) => {
|
||||||
|
st.position = i
|
||||||
|
})
|
||||||
|
setSubtasks(newSubtasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMoveSubtaskDown = (index) => {
|
||||||
|
if (index === subtasks.length - 1) return // Нельзя переместить последний элемент вниз
|
||||||
|
const newSubtasks = [...subtasks]
|
||||||
|
const temp = newSubtasks[index]
|
||||||
|
newSubtasks[index] = newSubtasks[index + 1]
|
||||||
|
newSubtasks[index + 1] = temp
|
||||||
|
// Обновляем позиции
|
||||||
|
newSubtasks.forEach((st, i) => {
|
||||||
|
st.position = i
|
||||||
|
})
|
||||||
|
setSubtasks(newSubtasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
@@ -657,10 +690,11 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
|||||||
value: parseFloat(r.value) || 0,
|
value: parseFloat(r.value) || 0,
|
||||||
use_progression: !!(progressionBase && r.use_progression)
|
use_progression: !!(progressionBase && r.use_progression)
|
||||||
})),
|
})),
|
||||||
subtasks: isTest ? [] : subtasks.map(st => ({
|
subtasks: isTest ? [] : subtasks.map((st, index) => ({
|
||||||
id: st.id || undefined,
|
id: st.id || undefined,
|
||||||
name: st.name.trim() || null,
|
name: st.name.trim() || null,
|
||||||
reward_message: st.reward_message.trim() || null,
|
reward_message: st.reward_message.trim() || null,
|
||||||
|
position: st.position !== undefined && st.position !== null ? st.position : index,
|
||||||
rewards: st.rewards.map(r => ({
|
rewards: st.rewards.map(r => ({
|
||||||
position: r.position,
|
position: r.position,
|
||||||
project_name: r.project_name.trim(),
|
project_name: r.project_name.trim(),
|
||||||
@@ -1073,6 +1107,30 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
|||||||
{subtasks.map((subtask, index) => (
|
{subtasks.map((subtask, index) => (
|
||||||
<div key={index} className="subtask-form-item">
|
<div key={index} className="subtask-form-item">
|
||||||
<div className="subtask-header-row">
|
<div className="subtask-header-row">
|
||||||
|
<div className="subtask-position-controls">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleMoveSubtaskUp(index)}
|
||||||
|
className="move-subtask-button"
|
||||||
|
disabled={index === 0}
|
||||||
|
title="Переместить вверх"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="18 15 12 9 6 15"></polyline>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleMoveSubtaskDown(index)}
|
||||||
|
className="move-subtask-button"
|
||||||
|
disabled={index === subtasks.length - 1}
|
||||||
|
title="Переместить вниз"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={subtask.name}
|
value={subtask.name}
|
||||||
|
|||||||
Reference in New Issue
Block a user