Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b09137eca5 | ||
|
|
b9ca845b9c | ||
|
|
a1dd2f2c00 | ||
|
|
a081bff35b |
@@ -135,6 +135,7 @@ func main() {
|
|||||||
db.MembershipRepo,
|
db.MembershipRepo,
|
||||||
db.NoteRepo,
|
db.NoteRepo,
|
||||||
db.CategoryRepo,
|
db.CategoryRepo,
|
||||||
|
db.TaskListRepo,
|
||||||
db.UserRepo,
|
db.UserRepo,
|
||||||
permissionService,
|
permissionService,
|
||||||
)
|
)
|
||||||
@@ -151,6 +152,7 @@ func main() {
|
|||||||
|
|
||||||
categoryService := services.NewCategoryService(
|
categoryService := services.NewCategoryService(
|
||||||
db.CategoryRepo,
|
db.CategoryRepo,
|
||||||
|
db.TaskListRepo,
|
||||||
db.MembershipRepo,
|
db.MembershipRepo,
|
||||||
db.NoteRepo,
|
db.NoteRepo,
|
||||||
permissionService,
|
permissionService,
|
||||||
@@ -158,6 +160,7 @@ func main() {
|
|||||||
|
|
||||||
taskService := services.NewTaskService(
|
taskService := services.NewTaskService(
|
||||||
db.TaskRepo,
|
db.TaskRepo,
|
||||||
|
db.TaskListRepo,
|
||||||
db.TaskStatusRepo,
|
db.TaskStatusRepo,
|
||||||
db.NoteRepo,
|
db.NoteRepo,
|
||||||
db.CategoryRepo,
|
db.CategoryRepo,
|
||||||
@@ -269,6 +272,11 @@ func main() {
|
|||||||
api.HandleFunc("/spaces/{spaceId}/categories/{categoryId}/move", categoryHandler.MoveCategory).Methods("PATCH")
|
api.HandleFunc("/spaces/{spaceId}/categories/{categoryId}/move", categoryHandler.MoveCategory).Methods("PATCH")
|
||||||
|
|
||||||
// Task endpoints
|
// Task endpoints
|
||||||
|
api.HandleFunc("/spaces/{spaceId}/task-lists", taskHandler.ListTaskLists).Methods("GET")
|
||||||
|
api.HandleFunc("/spaces/{spaceId}/task-lists", taskHandler.CreateTaskList).Methods("POST")
|
||||||
|
api.HandleFunc("/spaces/{spaceId}/task-lists/{taskListId}", taskHandler.UpdateTaskList).Methods("PUT")
|
||||||
|
api.HandleFunc("/spaces/{spaceId}/task-lists/{taskListId}", taskHandler.DeleteTaskList).Methods("DELETE")
|
||||||
|
|
||||||
api.HandleFunc("/spaces/{spaceId}/tasks", taskHandler.ListTasks).Methods("GET")
|
api.HandleFunc("/spaces/{spaceId}/tasks", taskHandler.ListTasks).Methods("GET")
|
||||||
api.HandleFunc("/spaces/{spaceId}/tasks", taskHandler.CreateTask).Methods("POST")
|
api.HandleFunc("/spaces/{spaceId}/tasks", taskHandler.CreateTask).Methods("POST")
|
||||||
api.HandleFunc("/spaces/{spaceId}/tasks/search", taskHandler.SearchTasks).Methods("GET")
|
api.HandleFunc("/spaces/{spaceId}/tasks/search", taskHandler.SearchTasks).Methods("GET")
|
||||||
|
|||||||
@@ -430,6 +430,7 @@ type CategoryTreeDTO struct {
|
|||||||
*CategoryDTO
|
*CategoryDTO
|
||||||
Subcategories []*CategoryTreeDTO `json:"subcategories"`
|
Subcategories []*CategoryTreeDTO `json:"subcategories"`
|
||||||
Notes []*NoteListItemDTO `json:"notes"`
|
Notes []*NoteListItemDTO `json:"notes"`
|
||||||
|
TaskLists []*TaskListDTO `json:"task_lists"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCategoryDTO creates a DTO from a category entity
|
// NewCategoryDTO creates a DTO from a category entity
|
||||||
@@ -458,7 +459,7 @@ func NewCategoryDTO(category *entities.Category) *CategoryDTO {
|
|||||||
type CreateTaskRequest struct {
|
type CreateTaskRequest struct {
|
||||||
Title string `json:"title" validate:"required,min=1,max=255"`
|
Title string `json:"title" validate:"required,min=1,max=255"`
|
||||||
Description string `json:"description" validate:"max=2000"`
|
Description string `json:"description" validate:"max=2000"`
|
||||||
CategoryID *string `json:"category_id,omitempty"`
|
TaskListID string `json:"task_list_id" validate:"required"`
|
||||||
StatusID string `json:"status_id" validate:"required"`
|
StatusID string `json:"status_id" validate:"required"`
|
||||||
ParentTaskID *string `json:"parent_task_id,omitempty"`
|
ParentTaskID *string `json:"parent_task_id,omitempty"`
|
||||||
NoteLinks []string `json:"note_links"`
|
NoteLinks []string `json:"note_links"`
|
||||||
@@ -468,7 +469,7 @@ type CreateTaskRequest struct {
|
|||||||
type UpdateTaskRequest struct {
|
type UpdateTaskRequest struct {
|
||||||
Title *string `json:"title,omitempty"`
|
Title *string `json:"title,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
CategoryID *string `json:"category_id,omitempty"`
|
TaskListID *string `json:"task_list_id,omitempty"`
|
||||||
StatusID *string `json:"status_id,omitempty"`
|
StatusID *string `json:"status_id,omitempty"`
|
||||||
ParentTaskID *string `json:"parent_task_id,omitempty"`
|
ParentTaskID *string `json:"parent_task_id,omitempty"`
|
||||||
NoteLinks []string `json:"note_links,omitempty"`
|
NoteLinks []string `json:"note_links,omitempty"`
|
||||||
@@ -490,7 +491,7 @@ type TaskDTO struct {
|
|||||||
SpaceID string `json:"space_id"`
|
SpaceID string `json:"space_id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
CategoryID *string `json:"category_id,omitempty"`
|
TaskListID string `json:"task_list_id"`
|
||||||
StatusID string `json:"status_id"`
|
StatusID string `json:"status_id"`
|
||||||
ParentTaskID *string `json:"parent_task_id,omitempty"`
|
ParentTaskID *string `json:"parent_task_id,omitempty"`
|
||||||
Depth int `json:"depth"`
|
Depth int `json:"depth"`
|
||||||
@@ -538,14 +539,35 @@ type TaskStatusDTO struct {
|
|||||||
UpdatedAt string `json:"updated_at"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTaskDTO creates a DTO from a task entity.
|
// CreateTaskListRequest represents task list creation input.
|
||||||
func NewTaskDTO(task *entities.Task) *TaskDTO {
|
type CreateTaskListRequest struct {
|
||||||
var categoryID *string
|
Name string `json:"name" validate:"required,min=1,max=120"`
|
||||||
if task.CategoryID != nil {
|
Description string `json:"description" validate:"max=500"`
|
||||||
id := task.CategoryID.Hex()
|
CategoryID *string `json:"category_id,omitempty"`
|
||||||
categoryID = &id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateTaskListRequest represents task list update input.
|
||||||
|
type UpdateTaskListRequest struct {
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
CategoryID *string `json:"category_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskListDTO represents a task list in API responses.
|
||||||
|
type TaskListDTO struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
SpaceID string `json:"space_id"`
|
||||||
|
CategoryID *string `json:"category_id,omitempty"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
CreatedBy string `json:"created_by"`
|
||||||
|
UpdatedBy string `json:"updated_by"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTaskDTO creates a DTO from a task entity.
|
||||||
|
func NewTaskDTO(task *entities.Task) *TaskDTO {
|
||||||
var parentTaskID *string
|
var parentTaskID *string
|
||||||
if task.ParentTaskID != nil {
|
if task.ParentTaskID != nil {
|
||||||
id := task.ParentTaskID.Hex()
|
id := task.ParentTaskID.Hex()
|
||||||
@@ -562,7 +584,7 @@ func NewTaskDTO(task *entities.Task) *TaskDTO {
|
|||||||
SpaceID: task.SpaceID.Hex(),
|
SpaceID: task.SpaceID.Hex(),
|
||||||
Title: task.Title,
|
Title: task.Title,
|
||||||
Description: task.Description,
|
Description: task.Description,
|
||||||
CategoryID: categoryID,
|
TaskListID: task.TaskListID.Hex(),
|
||||||
StatusID: task.StatusID.Hex(),
|
StatusID: task.StatusID.Hex(),
|
||||||
ParentTaskID: parentTaskID,
|
ParentTaskID: parentTaskID,
|
||||||
Depth: task.Depth,
|
Depth: task.Depth,
|
||||||
@@ -574,6 +596,27 @@ func NewTaskDTO(task *entities.Task) *TaskDTO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewTaskListDTO creates a DTO from a task list entity.
|
||||||
|
func NewTaskListDTO(taskList *entities.TaskList) *TaskListDTO {
|
||||||
|
var categoryID *string
|
||||||
|
if taskList.CategoryID != nil {
|
||||||
|
id := taskList.CategoryID.Hex()
|
||||||
|
categoryID = &id
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TaskListDTO{
|
||||||
|
ID: taskList.ID.Hex(),
|
||||||
|
SpaceID: taskList.SpaceID.Hex(),
|
||||||
|
CategoryID: categoryID,
|
||||||
|
Name: taskList.Name,
|
||||||
|
Description: taskList.Description,
|
||||||
|
CreatedBy: taskList.CreatedBy.Hex(),
|
||||||
|
UpdatedBy: taskList.UpdatedBy.Hex(),
|
||||||
|
CreatedAt: taskList.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
|
UpdatedAt: taskList.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NewTaskStatusDTO creates a DTO from a task status entity.
|
// NewTaskStatusDTO creates a DTO from a task status entity.
|
||||||
func NewTaskStatusDTO(status *entities.TaskStatus) *TaskStatusDTO {
|
func NewTaskStatusDTO(status *entities.TaskStatus) *TaskStatusDTO {
|
||||||
return &TaskStatusDTO{
|
return &TaskStatusDTO{
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
// CategoryService handles category operations
|
// CategoryService handles category operations
|
||||||
type CategoryService struct {
|
type CategoryService struct {
|
||||||
categoryRepo repositories.CategoryRepository
|
categoryRepo repositories.CategoryRepository
|
||||||
|
taskListRepo repositories.TaskListRepository
|
||||||
membershipRepo repositories.MembershipRepository
|
membershipRepo repositories.MembershipRepository
|
||||||
noteRepo repositories.NoteRepository
|
noteRepo repositories.NoteRepository
|
||||||
permissionService *PermissionService
|
permissionService *PermissionService
|
||||||
@@ -23,12 +24,14 @@ type CategoryService struct {
|
|||||||
// NewCategoryService creates a new category service
|
// NewCategoryService creates a new category service
|
||||||
func NewCategoryService(
|
func NewCategoryService(
|
||||||
categoryRepo repositories.CategoryRepository,
|
categoryRepo repositories.CategoryRepository,
|
||||||
|
taskListRepo repositories.TaskListRepository,
|
||||||
membershipRepo repositories.MembershipRepository,
|
membershipRepo repositories.MembershipRepository,
|
||||||
noteRepo repositories.NoteRepository,
|
noteRepo repositories.NoteRepository,
|
||||||
permissionService *PermissionService,
|
permissionService *PermissionService,
|
||||||
) *CategoryService {
|
) *CategoryService {
|
||||||
return &CategoryService{
|
return &CategoryService{
|
||||||
categoryRepo: categoryRepo,
|
categoryRepo: categoryRepo,
|
||||||
|
taskListRepo: taskListRepo,
|
||||||
membershipRepo: membershipRepo,
|
membershipRepo: membershipRepo,
|
||||||
noteRepo: noteRepo,
|
noteRepo: noteRepo,
|
||||||
permissionService: permissionService,
|
permissionService: permissionService,
|
||||||
@@ -134,6 +137,14 @@ func (s *CategoryService) buildCategoryTree(ctx context.Context, category *entit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get task lists in this category
|
||||||
|
taskLists, err := s.taskListRepo.ListTaskListsByCategory(ctx, spaceID, category.ID)
|
||||||
|
if err == nil {
|
||||||
|
for _, taskList := range taskLists {
|
||||||
|
tree.TaskLists = append(tree.TaskLists, dto.NewTaskListDTO(taskList))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return tree, nil
|
return tree, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type SpaceService struct {
|
|||||||
membershipRepo repositories.MembershipRepository
|
membershipRepo repositories.MembershipRepository
|
||||||
noteRepo repositories.NoteRepository
|
noteRepo repositories.NoteRepository
|
||||||
categoryRepo repositories.CategoryRepository
|
categoryRepo repositories.CategoryRepository
|
||||||
|
taskListRepo repositories.TaskListRepository
|
||||||
userRepo repositories.UserRepository
|
userRepo repositories.UserRepository
|
||||||
permissionService *PermissionService
|
permissionService *PermissionService
|
||||||
}
|
}
|
||||||
@@ -27,6 +28,7 @@ func NewSpaceService(
|
|||||||
membershipRepo repositories.MembershipRepository,
|
membershipRepo repositories.MembershipRepository,
|
||||||
noteRepo repositories.NoteRepository,
|
noteRepo repositories.NoteRepository,
|
||||||
categoryRepo repositories.CategoryRepository,
|
categoryRepo repositories.CategoryRepository,
|
||||||
|
taskListRepo repositories.TaskListRepository,
|
||||||
userRepo repositories.UserRepository,
|
userRepo repositories.UserRepository,
|
||||||
permissionService *PermissionService,
|
permissionService *PermissionService,
|
||||||
) *SpaceService {
|
) *SpaceService {
|
||||||
@@ -35,6 +37,7 @@ func NewSpaceService(
|
|||||||
membershipRepo: membershipRepo,
|
membershipRepo: membershipRepo,
|
||||||
noteRepo: noteRepo,
|
noteRepo: noteRepo,
|
||||||
categoryRepo: categoryRepo,
|
categoryRepo: categoryRepo,
|
||||||
|
taskListRepo: taskListRepo,
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
permissionService: permissionService,
|
permissionService: permissionService,
|
||||||
}
|
}
|
||||||
@@ -180,6 +183,9 @@ func (s *SpaceService) DeleteSpace(ctx context.Context, spaceID, userID bson.Obj
|
|||||||
if err := s.categoryRepo.DeleteCategoriesBySpaceID(ctx, spaceID); err != nil {
|
if err := s.categoryRepo.DeleteCategoriesBySpaceID(ctx, spaceID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := s.taskListRepo.DeleteTaskListsBySpaceID(ctx, spaceID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := s.membershipRepo.DeleteMembershipsBySpaceID(ctx, spaceID); err != nil {
|
if err := s.membershipRepo.DeleteMembershipsBySpaceID(ctx, spaceID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
// TaskService handles task and task status operations.
|
// TaskService handles task and task status operations.
|
||||||
type TaskService struct {
|
type TaskService struct {
|
||||||
taskRepo repositories.TaskRepository
|
taskRepo repositories.TaskRepository
|
||||||
|
taskListRepo repositories.TaskListRepository
|
||||||
taskStatusRepo repositories.TaskStatusRepository
|
taskStatusRepo repositories.TaskStatusRepository
|
||||||
noteRepo repositories.NoteRepository
|
noteRepo repositories.NoteRepository
|
||||||
categoryRepo repositories.CategoryRepository
|
categoryRepo repositories.CategoryRepository
|
||||||
@@ -27,6 +28,7 @@ type TaskService struct {
|
|||||||
// NewTaskService creates a task service.
|
// NewTaskService creates a task service.
|
||||||
func NewTaskService(
|
func NewTaskService(
|
||||||
taskRepo repositories.TaskRepository,
|
taskRepo repositories.TaskRepository,
|
||||||
|
taskListRepo repositories.TaskListRepository,
|
||||||
taskStatusRepo repositories.TaskStatusRepository,
|
taskStatusRepo repositories.TaskStatusRepository,
|
||||||
noteRepo repositories.NoteRepository,
|
noteRepo repositories.NoteRepository,
|
||||||
categoryRepo repositories.CategoryRepository,
|
categoryRepo repositories.CategoryRepository,
|
||||||
@@ -35,6 +37,7 @@ func NewTaskService(
|
|||||||
) *TaskService {
|
) *TaskService {
|
||||||
return &TaskService{
|
return &TaskService{
|
||||||
taskRepo: taskRepo,
|
taskRepo: taskRepo,
|
||||||
|
taskListRepo: taskListRepo,
|
||||||
taskStatusRepo: taskStatusRepo,
|
taskStatusRepo: taskStatusRepo,
|
||||||
noteRepo: noteRepo,
|
noteRepo: noteRepo,
|
||||||
categoryRepo: categoryRepo,
|
categoryRepo: categoryRepo,
|
||||||
@@ -121,13 +124,10 @@ func toObjectIDs(hexIDs []string) ([]bson.ObjectID, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TaskService) validateCategory(ctx context.Context, spaceID bson.ObjectID, categoryID *bson.ObjectID) error {
|
func (s *TaskService) validateTaskList(ctx context.Context, spaceID, taskListID bson.ObjectID) error {
|
||||||
if categoryID == nil {
|
taskList, err := s.taskListRepo.GetTaskListByID(ctx, taskListID)
|
||||||
return nil
|
if err != nil || taskList.SpaceID != spaceID {
|
||||||
}
|
return errors.New("invalid task list")
|
||||||
category, err := s.categoryRepo.GetCategoryByID(ctx, *categoryID)
|
|
||||||
if err != nil || category.SpaceID != spaceID {
|
|
||||||
return errors.New("invalid category")
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -209,14 +209,6 @@ func (s *TaskService) CreateTask(ctx context.Context, spaceID, userID bson.Objec
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
categoryID, err := toObjectIDPtr(req.CategoryID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("invalid category")
|
|
||||||
}
|
|
||||||
if err := s.validateCategory(ctx, spaceID, categoryID); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
parentTaskID, err := toObjectIDPtr(req.ParentTaskID)
|
parentTaskID, err := toObjectIDPtr(req.ParentTaskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("invalid parent task")
|
return nil, errors.New("invalid parent task")
|
||||||
@@ -226,6 +218,14 @@ func (s *TaskService) CreateTask(ctx context.Context, spaceID, userID bson.Objec
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
taskListID, err := bson.ObjectIDFromHex(strings.TrimSpace(req.TaskListID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("invalid task list")
|
||||||
|
}
|
||||||
|
if err := s.validateTaskList(ctx, spaceID, taskListID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
noteLinks, err := toObjectIDs(req.NoteLinks)
|
noteLinks, err := toObjectIDs(req.NoteLinks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -258,11 +258,19 @@ func (s *TaskService) CreateTask(ctx context.Context, spaceID, userID bson.Objec
|
|||||||
statusID = parsedStatusID
|
statusID = parsedStatusID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if parentTaskID != nil {
|
||||||
|
parent, parentErr := s.taskRepo.GetTaskByID(ctx, *parentTaskID)
|
||||||
|
if parentErr != nil || parent.SpaceID != spaceID {
|
||||||
|
return nil, errors.New("invalid parent task")
|
||||||
|
}
|
||||||
|
taskListID = parent.TaskListID
|
||||||
|
}
|
||||||
|
|
||||||
task := &entities.Task{
|
task := &entities.Task{
|
||||||
SpaceID: spaceID,
|
SpaceID: spaceID,
|
||||||
Title: strings.TrimSpace(req.Title),
|
Title: strings.TrimSpace(req.Title),
|
||||||
Description: strings.TrimSpace(req.Description),
|
Description: strings.TrimSpace(req.Description),
|
||||||
CategoryID: categoryID,
|
TaskListID: taskListID,
|
||||||
StatusID: statusID,
|
StatusID: statusID,
|
||||||
ParentTaskID: parentTaskID,
|
ParentTaskID: parentTaskID,
|
||||||
Depth: depth,
|
Depth: depth,
|
||||||
@@ -318,7 +326,7 @@ func (s *TaskService) GetTaskByID(ctx context.Context, spaceID, taskID, userID b
|
|||||||
func (s *TaskService) ListTasks(
|
func (s *TaskService) ListTasks(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
spaceID, userID bson.ObjectID,
|
spaceID, userID bson.ObjectID,
|
||||||
categoryID, statusID, parentTaskID *string,
|
taskListID, statusID, parentTaskID *string,
|
||||||
) ([]*dto.TaskDTO, error) {
|
) ([]*dto.TaskDTO, error) {
|
||||||
if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil {
|
if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -328,12 +336,12 @@ func (s *TaskService) ListTasks(
|
|||||||
}
|
}
|
||||||
|
|
||||||
filters := map[string]any{}
|
filters := map[string]any{}
|
||||||
if categoryID != nil && strings.TrimSpace(*categoryID) != "" {
|
if taskListID != nil && strings.TrimSpace(*taskListID) != "" {
|
||||||
id, err := bson.ObjectIDFromHex(*categoryID)
|
id, err := bson.ObjectIDFromHex(*taskListID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("invalid category filter")
|
return nil, errors.New("invalid task list filter")
|
||||||
}
|
}
|
||||||
filters["category_id"] = id
|
filters["task_list_id"] = id
|
||||||
}
|
}
|
||||||
if statusID != nil && strings.TrimSpace(*statusID) != "" {
|
if statusID != nil && strings.TrimSpace(*statusID) != "" {
|
||||||
id, err := bson.ObjectIDFromHex(*statusID)
|
id, err := bson.ObjectIDFromHex(*statusID)
|
||||||
@@ -453,19 +461,21 @@ func (s *TaskService) UpdateTask(ctx context.Context, spaceID, taskID, userID bs
|
|||||||
task.Description = strings.TrimSpace(*req.Description)
|
task.Description = strings.TrimSpace(*req.Description)
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.CategoryID != nil {
|
if req.TaskListID != nil {
|
||||||
if strings.TrimSpace(*req.CategoryID) == "" {
|
if strings.TrimSpace(*req.TaskListID) == "" {
|
||||||
task.CategoryID = nil
|
return nil, errors.New("task list is required")
|
||||||
} else {
|
}
|
||||||
categoryID, parseErr := bson.ObjectIDFromHex(*req.CategoryID)
|
taskListID, parseErr := bson.ObjectIDFromHex(*req.TaskListID)
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
return nil, errors.New("invalid category")
|
return nil, errors.New("invalid task list")
|
||||||
}
|
}
|
||||||
task.CategoryID = &categoryID
|
if task.ParentTaskID != nil {
|
||||||
|
return nil, errors.New("subtasks inherit task list from parent")
|
||||||
}
|
}
|
||||||
if err := s.validateCategory(ctx, spaceID, task.CategoryID); err != nil {
|
if err := s.validateTaskList(ctx, spaceID, taskListID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
task.TaskListID = taskListID
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.ParentTaskID != nil {
|
if req.ParentTaskID != nil {
|
||||||
@@ -486,6 +496,11 @@ func (s *TaskService) UpdateTask(ctx context.Context, spaceID, taskID, userID bs
|
|||||||
}
|
}
|
||||||
task.ParentTaskID = &parentID
|
task.ParentTaskID = &parentID
|
||||||
task.Depth = depth
|
task.Depth = depth
|
||||||
|
parentTask, parentErr := s.taskRepo.GetTaskByID(ctx, parentID)
|
||||||
|
if parentErr != nil || parentTask.SpaceID != spaceID {
|
||||||
|
return nil, errors.New("invalid parent task")
|
||||||
|
}
|
||||||
|
task.TaskListID = parentTask.TaskListID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -670,6 +685,140 @@ func (s *TaskService) UnlinkNoteFromTask(ctx context.Context, spaceID, taskID, n
|
|||||||
return dto.NewTaskDTO(task), nil
|
return dto.NewTaskDTO(task), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TaskService) ListTaskLists(ctx context.Context, spaceID, userID bson.ObjectID) ([]*dto.TaskListDTO, error) {
|
||||||
|
if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lists, err := s.taskListRepo.ListTaskLists(ctx, spaceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := make([]*dto.TaskListDTO, 0, len(lists))
|
||||||
|
for _, list := range lists {
|
||||||
|
result = append(result, dto.NewTaskListDTO(list))
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TaskService) CreateTaskList(ctx context.Context, spaceID, userID bson.ObjectID, req *dto.CreateTaskListRequest) (*dto.TaskListDTO, error) {
|
||||||
|
if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
hasPermission, err := s.hasTaskPermission(ctx, userID, spaceID, "tasks.create")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !hasPermission {
|
||||||
|
return nil, errors.New("insufficient permissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
name := strings.TrimSpace(req.Name)
|
||||||
|
if name == "" {
|
||||||
|
return nil, errors.New("task list name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var categoryID *bson.ObjectID
|
||||||
|
if req.CategoryID != nil && strings.TrimSpace(*req.CategoryID) != "" {
|
||||||
|
id, parseErr := bson.ObjectIDFromHex(*req.CategoryID)
|
||||||
|
if parseErr != nil {
|
||||||
|
return nil, errors.New("invalid category")
|
||||||
|
}
|
||||||
|
category, categoryErr := s.categoryRepo.GetCategoryByID(ctx, id)
|
||||||
|
if categoryErr != nil || category.SpaceID != spaceID {
|
||||||
|
return nil, errors.New("invalid category")
|
||||||
|
}
|
||||||
|
categoryID = &id
|
||||||
|
}
|
||||||
|
|
||||||
|
list := &entities.TaskList{
|
||||||
|
SpaceID: spaceID,
|
||||||
|
CategoryID: categoryID,
|
||||||
|
Name: name,
|
||||||
|
Description: strings.TrimSpace(req.Description),
|
||||||
|
CreatedBy: userID,
|
||||||
|
UpdatedBy: userID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.taskListRepo.CreateTaskList(ctx, list); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dto.NewTaskListDTO(list), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TaskService) UpdateTaskList(ctx context.Context, spaceID, taskListID, userID bson.ObjectID, req *dto.UpdateTaskListRequest) (*dto.TaskListDTO, error) {
|
||||||
|
if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
hasPermission, err := s.hasTaskPermission(ctx, userID, spaceID, "tasks.edit")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !hasPermission {
|
||||||
|
return nil, errors.New("insufficient permissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := s.taskListRepo.GetTaskListByID(ctx, taskListID)
|
||||||
|
if err != nil || list.SpaceID != spaceID {
|
||||||
|
return nil, errors.New("task list not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name != nil {
|
||||||
|
name := strings.TrimSpace(*req.Name)
|
||||||
|
if name == "" {
|
||||||
|
return nil, errors.New("task list name is required")
|
||||||
|
}
|
||||||
|
list.Name = name
|
||||||
|
}
|
||||||
|
if req.Description != nil {
|
||||||
|
list.Description = strings.TrimSpace(*req.Description)
|
||||||
|
}
|
||||||
|
if req.CategoryID != nil {
|
||||||
|
if strings.TrimSpace(*req.CategoryID) == "" {
|
||||||
|
list.CategoryID = nil
|
||||||
|
} else {
|
||||||
|
categoryID, parseErr := bson.ObjectIDFromHex(*req.CategoryID)
|
||||||
|
if parseErr != nil {
|
||||||
|
return nil, errors.New("invalid category")
|
||||||
|
}
|
||||||
|
category, categoryErr := s.categoryRepo.GetCategoryByID(ctx, categoryID)
|
||||||
|
if categoryErr != nil || category.SpaceID != spaceID {
|
||||||
|
return nil, errors.New("invalid category")
|
||||||
|
}
|
||||||
|
list.CategoryID = &categoryID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list.UpdatedBy = userID
|
||||||
|
if err := s.taskListRepo.UpdateTaskList(ctx, list); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dto.NewTaskListDTO(list), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TaskService) DeleteTaskList(ctx context.Context, spaceID, taskListID, userID bson.ObjectID) error {
|
||||||
|
if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hasPermission, err := s.hasTaskPermission(ctx, userID, spaceID, "tasks.delete")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !hasPermission {
|
||||||
|
return errors.New("insufficient permissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := s.taskListRepo.GetTaskListByID(ctx, taskListID)
|
||||||
|
if err != nil || list.SpaceID != spaceID {
|
||||||
|
return errors.New("task list not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.taskRepo.DeleteTasksByTaskListID(ctx, taskListID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.taskListRepo.DeleteTaskList(ctx, taskListID)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *TaskService) ListStatuses(ctx context.Context, spaceID, userID bson.ObjectID) ([]*dto.TaskStatusDTO, error) {
|
func (s *TaskService) ListStatuses(ctx context.Context, spaceID, userID bson.ObjectID) ([]*dto.TaskStatusDTO, error) {
|
||||||
if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil {
|
if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type Task struct {
|
|||||||
SpaceID bson.ObjectID `bson:"space_id"`
|
SpaceID bson.ObjectID `bson:"space_id"`
|
||||||
Title string `bson:"title"`
|
Title string `bson:"title"`
|
||||||
Description string `bson:"description"`
|
Description string `bson:"description"`
|
||||||
CategoryID *bson.ObjectID `bson:"category_id,omitempty"`
|
TaskListID bson.ObjectID `bson:"task_list_id"`
|
||||||
StatusID bson.ObjectID `bson:"status_id"`
|
StatusID bson.ObjectID `bson:"status_id"`
|
||||||
ParentTaskID *bson.ObjectID `bson:"parent_task_id,omitempty"`
|
ParentTaskID *bson.ObjectID `bson:"parent_task_id,omitempty"`
|
||||||
Depth int `bson:"depth"`
|
Depth int `bson:"depth"`
|
||||||
@@ -35,3 +35,16 @@ type TaskStatus struct {
|
|||||||
CreatedAt time.Time `bson:"created_at"`
|
CreatedAt time.Time `bson:"created_at"`
|
||||||
UpdatedAt time.Time `bson:"updated_at"`
|
UpdatedAt time.Time `bson:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TaskList groups tasks under a named list that can be attached to a category.
|
||||||
|
type TaskList struct {
|
||||||
|
ID bson.ObjectID `bson:"_id,omitempty"`
|
||||||
|
SpaceID bson.ObjectID `bson:"space_id"`
|
||||||
|
CategoryID *bson.ObjectID `bson:"category_id,omitempty"`
|
||||||
|
Name string `bson:"name"`
|
||||||
|
Description string `bson:"description,omitempty"`
|
||||||
|
CreatedBy bson.ObjectID `bson:"created_by"`
|
||||||
|
UpdatedBy bson.ObjectID `bson:"updated_by"`
|
||||||
|
CreatedAt time.Time `bson:"created_at"`
|
||||||
|
UpdatedAt time.Time `bson:"updated_at"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -225,10 +225,22 @@ type TaskRepository interface {
|
|||||||
SearchTasks(ctx context.Context, spaceID bson.ObjectID, query string) ([]*entities.Task, error)
|
SearchTasks(ctx context.Context, spaceID bson.ObjectID, query string) ([]*entities.Task, error)
|
||||||
UpdateTask(ctx context.Context, task *entities.Task) error
|
UpdateTask(ctx context.Context, task *entities.Task) error
|
||||||
DeleteTask(ctx context.Context, id bson.ObjectID) error
|
DeleteTask(ctx context.Context, id bson.ObjectID) error
|
||||||
|
DeleteTasksByTaskListID(ctx context.Context, taskListID bson.ObjectID) error
|
||||||
DeleteTasksBySpaceID(ctx context.Context, spaceID bson.ObjectID) error
|
DeleteTasksBySpaceID(ctx context.Context, spaceID bson.ObjectID) error
|
||||||
CountChildren(ctx context.Context, parentTaskID bson.ObjectID) (int64, error)
|
CountChildren(ctx context.Context, parentTaskID bson.ObjectID) (int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TaskListRepository defines task list operations.
|
||||||
|
type TaskListRepository interface {
|
||||||
|
CreateTaskList(ctx context.Context, list *entities.TaskList) error
|
||||||
|
GetTaskListByID(ctx context.Context, id bson.ObjectID) (*entities.TaskList, error)
|
||||||
|
ListTaskLists(ctx context.Context, spaceID bson.ObjectID) ([]*entities.TaskList, error)
|
||||||
|
ListTaskListsByCategory(ctx context.Context, spaceID bson.ObjectID, categoryID bson.ObjectID) ([]*entities.TaskList, error)
|
||||||
|
UpdateTaskList(ctx context.Context, list *entities.TaskList) error
|
||||||
|
DeleteTaskList(ctx context.Context, id bson.ObjectID) error
|
||||||
|
DeleteTaskListsBySpaceID(ctx context.Context, spaceID bson.ObjectID) error
|
||||||
|
}
|
||||||
|
|
||||||
// TaskStatusRepository defines task status operations
|
// TaskStatusRepository defines task status operations
|
||||||
type TaskStatusRepository interface {
|
type TaskStatusRepository interface {
|
||||||
CreateStatus(ctx context.Context, status *entities.TaskStatus) error
|
CreateStatus(ctx context.Context, status *entities.TaskStatus) error
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type Database struct {
|
|||||||
MembershipRepo *MembershipRepository
|
MembershipRepo *MembershipRepository
|
||||||
NoteRepo *NoteRepository
|
NoteRepo *NoteRepository
|
||||||
CategoryRepo *CategoryRepository
|
CategoryRepo *CategoryRepository
|
||||||
|
TaskListRepo *TaskListRepository
|
||||||
TaskRepo *TaskRepository
|
TaskRepo *TaskRepository
|
||||||
TaskStatusRepo *TaskStatusRepository
|
TaskStatusRepo *TaskStatusRepository
|
||||||
RevisionRepo *NoteRevisionRepository
|
RevisionRepo *NoteRevisionRepository
|
||||||
@@ -49,6 +50,7 @@ func NewDatabase(ctx context.Context, mongoURL string) (*Database, error) {
|
|||||||
MembershipRepo: NewMembershipRepository(db),
|
MembershipRepo: NewMembershipRepository(db),
|
||||||
NoteRepo: NewNoteRepository(db),
|
NoteRepo: NewNoteRepository(db),
|
||||||
CategoryRepo: NewCategoryRepository(db),
|
CategoryRepo: NewCategoryRepository(db),
|
||||||
|
TaskListRepo: NewTaskListRepository(db),
|
||||||
TaskRepo: NewTaskRepository(db),
|
TaskRepo: NewTaskRepository(db),
|
||||||
TaskStatusRepo: NewTaskStatusRepository(db),
|
TaskStatusRepo: NewTaskStatusRepository(db),
|
||||||
RevisionRepo: NewNoteRevisionRepository(db),
|
RevisionRepo: NewNoteRevisionRepository(db),
|
||||||
@@ -87,6 +89,9 @@ func (d *Database) EnsureIndexes(ctx context.Context) error {
|
|||||||
if err := d.TaskRepo.EnsureIndexes(ctx); err != nil {
|
if err := d.TaskRepo.EnsureIndexes(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := d.TaskListRepo.EnsureIndexes(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := d.TaskStatusRepo.EnsureIndexes(ctx); err != nil {
|
if err := d.TaskStatusRepo.EnsureIndexes(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ func (r *TaskRepository) DeleteTask(ctx context.Context, id bson.ObjectID) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *TaskRepository) DeleteTasksByTaskListID(ctx context.Context, taskListID bson.ObjectID) error {
|
||||||
|
_, err := r.collection.DeleteMany(ctx, bson.M{"task_list_id": taskListID})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *TaskRepository) DeleteTasksBySpaceID(ctx context.Context, spaceID bson.ObjectID) error {
|
func (r *TaskRepository) DeleteTasksBySpaceID(ctx context.Context, spaceID bson.ObjectID) error {
|
||||||
_, err := r.collection.DeleteMany(ctx, bson.M{"space_id": spaceID})
|
_, err := r.collection.DeleteMany(ctx, bson.M{"space_id": spaceID})
|
||||||
return err
|
return err
|
||||||
@@ -108,13 +113,95 @@ func (r *TaskRepository) EnsureIndexes(ctx context.Context) error {
|
|||||||
_, err := r.collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
|
_, err := r.collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||||
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "updated_at", Value: -1}}},
|
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "updated_at", Value: -1}}},
|
||||||
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "status_id", Value: 1}}},
|
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "status_id", Value: 1}}},
|
||||||
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "category_id", Value: 1}}},
|
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "task_list_id", Value: 1}}},
|
||||||
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "parent_task_id", Value: 1}}},
|
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "parent_task_id", Value: 1}}},
|
||||||
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "note_links", Value: 1}}},
|
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "note_links", Value: 1}}},
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TaskListRepository implements task list data access.
|
||||||
|
type TaskListRepository struct {
|
||||||
|
collection *mongo.Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTaskListRepository creates a new task list repository.
|
||||||
|
func NewTaskListRepository(db *mongo.Database) *TaskListRepository {
|
||||||
|
return &TaskListRepository{collection: db.Collection("task_lists")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TaskListRepository) CreateTaskList(ctx context.Context, list *entities.TaskList) error {
|
||||||
|
list.ID = bson.NewObjectID()
|
||||||
|
list.CreatedAt = time.Now()
|
||||||
|
list.UpdatedAt = time.Now()
|
||||||
|
_, err := r.collection.InsertOne(ctx, list)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TaskListRepository) GetTaskListByID(ctx context.Context, id bson.ObjectID) (*entities.TaskList, error) {
|
||||||
|
var list entities.TaskList
|
||||||
|
err := r.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&list)
|
||||||
|
if err != nil {
|
||||||
|
if err == mongo.ErrNoDocuments {
|
||||||
|
return nil, errors.New("task list not found")
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TaskListRepository) ListTaskLists(ctx context.Context, spaceID bson.ObjectID) ([]*entities.TaskList, error) {
|
||||||
|
cursor, err := r.collection.Find(ctx, bson.M{"space_id": spaceID}, options.Find().SetSort(bson.D{{Key: "name", Value: 1}}))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer cursor.Close(ctx)
|
||||||
|
|
||||||
|
var lists []*entities.TaskList
|
||||||
|
if err := cursor.All(ctx, &lists); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return lists, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TaskListRepository) ListTaskListsByCategory(ctx context.Context, spaceID bson.ObjectID, categoryID bson.ObjectID) ([]*entities.TaskList, error) {
|
||||||
|
cursor, err := r.collection.Find(ctx, bson.M{"space_id": spaceID, "category_id": categoryID}, options.Find().SetSort(bson.D{{Key: "name", Value: 1}}))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer cursor.Close(ctx)
|
||||||
|
|
||||||
|
var lists []*entities.TaskList
|
||||||
|
if err := cursor.All(ctx, &lists); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return lists, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TaskListRepository) UpdateTaskList(ctx context.Context, list *entities.TaskList) error {
|
||||||
|
list.UpdatedAt = time.Now()
|
||||||
|
_, err := r.collection.ReplaceOne(ctx, bson.M{"_id": list.ID}, list)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TaskListRepository) DeleteTaskList(ctx context.Context, id bson.ObjectID) error {
|
||||||
|
_, err := r.collection.DeleteOne(ctx, bson.M{"_id": id})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TaskListRepository) DeleteTaskListsBySpaceID(ctx context.Context, spaceID bson.ObjectID) error {
|
||||||
|
_, err := r.collection.DeleteMany(ctx, bson.M{"space_id": spaceID})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TaskListRepository) EnsureIndexes(ctx context.Context) error {
|
||||||
|
_, err := r.collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||||
|
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "name", Value: 1}}, Options: options.Index().SetUnique(true)},
|
||||||
|
{Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "category_id", Value: 1}}},
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// TaskStatusRepository implements task status data access.
|
// TaskStatusRepository implements task status data access.
|
||||||
type TaskStatusRepository struct {
|
type TaskStatusRepository struct {
|
||||||
collection *mongo.Collection
|
collection *mongo.Collection
|
||||||
|
|||||||
@@ -64,15 +64,15 @@ func (h *TaskHandler) ListTasks(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
categoryID := strings.TrimSpace(r.URL.Query().Get("categoryId"))
|
taskListID := strings.TrimSpace(r.URL.Query().Get("taskListId"))
|
||||||
statusID := strings.TrimSpace(r.URL.Query().Get("statusId"))
|
statusID := strings.TrimSpace(r.URL.Query().Get("statusId"))
|
||||||
parentTaskID := strings.TrimSpace(r.URL.Query().Get("parentTaskId"))
|
parentTaskID := strings.TrimSpace(r.URL.Query().Get("parentTaskId"))
|
||||||
|
|
||||||
categoryFilter := &categoryID
|
taskListFilter := &taskListID
|
||||||
statusFilter := &statusID
|
statusFilter := &statusID
|
||||||
parentFilter := &parentTaskID
|
parentFilter := &parentTaskID
|
||||||
if categoryID == "" {
|
if taskListID == "" {
|
||||||
categoryFilter = nil
|
taskListFilter = nil
|
||||||
}
|
}
|
||||||
if statusID == "" {
|
if statusID == "" {
|
||||||
statusFilter = nil
|
statusFilter = nil
|
||||||
@@ -81,7 +81,7 @@ func (h *TaskHandler) ListTasks(w http.ResponseWriter, r *http.Request) {
|
|||||||
parentFilter = nil
|
parentFilter = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks, err := h.taskService.ListTasks(r.Context(), spaceID, userID, categoryFilter, statusFilter, parentFilter)
|
tasks, err := h.taskService.ListTasks(r.Context(), spaceID, userID, taskListFilter, statusFilter, parentFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -91,6 +91,94 @@ func (h *TaskHandler) ListTasks(w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(tasks)
|
json.NewEncoder(w).Encode(tasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *TaskHandler) ListTaskLists(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID, spaceID, err := parseIDsFromRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lists, err := h.taskService.ListTaskLists(r.Context(), spaceID, userID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(lists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TaskHandler) CreateTaskList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID, spaceID, err := parseIDsFromRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req dto.CreateTaskListRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := h.taskService.CreateTaskList(r.Context(), spaceID, userID, &req)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TaskHandler) UpdateTaskList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID, spaceID, err := parseIDsFromRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
taskListID, err := bson.ObjectIDFromHex(mux.Vars(r)["taskListId"])
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid task list id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req dto.UpdateTaskListRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := h.taskService.UpdateTaskList(r.Context(), spaceID, taskListID, userID, &req)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TaskHandler) DeleteTaskList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID, spaceID, err := parseIDsFromRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
taskListID, err := bson.ObjectIDFromHex(mux.Vars(r)["taskListId"])
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid task list id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.taskService.DeleteTaskList(r.Context(), spaceID, taskListID, userID); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *TaskHandler) SearchTasks(w http.ResponseWriter, r *http.Request) {
|
func (h *TaskHandler) SearchTasks(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, spaceID, err := parseIDsFromRequest(r)
|
userID, spaceID, err := parseIDsFromRequest(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app" class="app-container">
|
<div id="app" class="app-container">
|
||||||
<nav v-if="!isPublicRoute && !isAuthRoute" ref="navbarRef" class="navbar navbar-dark bg-dark sticky-top">
|
<nav v-if="!isPublicRoute && !isAuthRoute" ref="navbarRef" class="navbar navbar-dark bg-dark sticky-top">
|
||||||
<div class="container-fluid app-navbar">
|
<div class="container-fluid app-navbar">
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
<!-- Search -->
|
<!-- Search -->
|
||||||
<div class="search-box nav-search" v-if="!isAdminRoute">
|
<div class="search-box nav-search" v-if="!isAdminRoute">
|
||||||
<input type="text" class="form-control" placeholder="Search notes..." v-model="searchQuery" @keyup.enter="performSearch" />
|
<input type="text" class="form-control" placeholder="Search notes & task lists..." v-model="searchQuery" @keyup.enter="performSearch" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Theme Toggle -->
|
<!-- Theme Toggle -->
|
||||||
@@ -98,6 +98,7 @@
|
|||||||
:on-add-subcategory="openCreateSubcategoryModal"
|
:on-add-subcategory="openCreateSubcategoryModal"
|
||||||
:on-edit-category="openEditCategoryModal"
|
:on-edit-category="openEditCategoryModal"
|
||||||
:on-delete-category="removeCategory"
|
:on-delete-category="removeCategory"
|
||||||
|
:on-select-task-list="selectTaskList"
|
||||||
:can-create-categories="canCreateCategories"
|
:can-create-categories="canCreateCategories"
|
||||||
:can-edit-categories="canEditCategories"
|
:can-edit-categories="canEditCategories"
|
||||||
:can-delete-categories="canDeleteCategories"
|
:can-delete-categories="canDeleteCategories"
|
||||||
@@ -128,9 +129,36 @@
|
|||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto d-flex align-items-center">
|
<div class="col-auto d-flex align-items-center">
|
||||||
<div class="btn-group me-2" role="group" aria-label="Workspace mode">
|
<div
|
||||||
<button type="button" class="btn action-button" :class="activeView === 'notes' ? 'btn-secondary' : 'btn-outline-secondary'" @click="activeView = 'notes'">Notes</button>
|
v-if="(activeView === 'notes' || activeView === 'tasks') && (canCreateNotes || canCreateTasks)"
|
||||||
<button type="button" class="btn action-button" :class="activeView === 'tasks' ? 'btn-secondary' : 'btn-outline-secondary'" @click="activeView = 'tasks'">Tasks</button>
|
ref="createDropdownRef"
|
||||||
|
class="dropdown me-2"
|
||||||
|
@mouseleave="showCreateMenu = false"
|
||||||
|
>
|
||||||
|
<button class="btn btn-primary dropdown-toggle action-button" type="button" aria-label="Create" title="Create" @click="toggleCreateMenu">
|
||||||
|
<i class="mdi mdi-plus-circle-outline me-1" aria-hidden="true"></i>
|
||||||
|
<span class="action-label">Create</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end" :class="{ show: showCreateMenu }">
|
||||||
|
<li v-if="activeView === 'notes' && canCreateNotes">
|
||||||
|
<button class="dropdown-item" type="button" @click="openCreateNoteModalFromMenu">
|
||||||
|
<i class="mdi mdi-note-plus-outline me-1" aria-hidden="true"></i>
|
||||||
|
New Note
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li v-if="activeView === 'notes' && canCreateTasks">
|
||||||
|
<button class="dropdown-item" type="button" @click="openCreateTaskListModalFromMenu">
|
||||||
|
<i class="mdi mdi-format-list-checkbox me-1" aria-hidden="true"></i>
|
||||||
|
New Task List
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li v-if="activeView === 'tasks' && canCreateTasks">
|
||||||
|
<button class="dropdown-item" type="button" @click="openTaskCreateModal">
|
||||||
|
<i class="mdi mdi-checkbox-marked-circle-plus-outline me-1" aria-hidden="true"></i>
|
||||||
|
New Task
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="activeView === 'notes' && (!selectedNote || isSearchRoute)" class="btn-group me-2 d-none d-md-flex" role="group" aria-label="View mode">
|
<div v-if="activeView === 'notes' && (!selectedNote || isSearchRoute)" class="btn-group me-2 d-none d-md-flex" role="group" aria-label="View mode">
|
||||||
<button
|
<button
|
||||||
@@ -174,14 +202,6 @@
|
|||||||
<i class="mdi mdi-share-variant-outline me-1" aria-hidden="true"></i>
|
<i class="mdi mdi-share-variant-outline me-1" aria-hidden="true"></i>
|
||||||
<span class="action-label">{{ shareCopied ? "Copied" : "Share" }}</span>
|
<span class="action-label">{{ shareCopied ? "Copied" : "Share" }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="activeView === 'notes' && canCreateNotes" class="btn btn-primary action-button" aria-label="New note" title="New note" @click="showCreateNoteModal = true">
|
|
||||||
<i class="mdi mdi-note-plus-outline me-1" aria-hidden="true"></i>
|
|
||||||
<span class="action-label">New Note</span>
|
|
||||||
</button>
|
|
||||||
<button v-if="activeView === 'tasks' && canCreateTasks" class="btn btn-primary action-button" aria-label="New task" title="New task" @click="openTaskCreateModal">
|
|
||||||
<i class="mdi mdi-checkbox-marked-circle-plus-outline me-1" aria-hidden="true"></i>
|
|
||||||
<span class="action-label">New Task</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -192,8 +212,8 @@
|
|||||||
v-if="activeView === 'tasks'"
|
v-if="activeView === 'tasks'"
|
||||||
:tasks="tasks"
|
:tasks="tasks"
|
||||||
:statuses="taskStatuses"
|
:statuses="taskStatuses"
|
||||||
:category-options="categoryOptions"
|
:selected-task-list="selectedTaskList"
|
||||||
@create-task="openTaskCreateModal"
|
:can-delete-task-list="canDeleteTasks"
|
||||||
@select-task="openTaskDetail"
|
@select-task="openTaskDetail"
|
||||||
@filter-change="applyTaskFilters"
|
@filter-change="applyTaskFilters"
|
||||||
@reorder-status="reorderTaskStatuses"
|
@reorder-status="reorderTaskStatuses"
|
||||||
@@ -201,15 +221,17 @@
|
|||||||
@rename-status="renameTaskStatus"
|
@rename-status="renameTaskStatus"
|
||||||
@delete-status="deleteTaskStatus"
|
@delete-status="deleteTaskStatus"
|
||||||
@update-task-status="updateTaskStatusFromBoard"
|
@update-task-status="updateTaskStatusFromBoard"
|
||||||
|
@delete-task-list="removeTaskList"
|
||||||
/>
|
/>
|
||||||
<SearchResultsPage
|
<SearchResultsPage
|
||||||
v-else-if="isSearchRoute"
|
v-else-if="isSearchRoute"
|
||||||
:notes="searchResults"
|
:items="searchItems"
|
||||||
:query="searchQuery"
|
:query="searchQuery"
|
||||||
:current-page="searchPage"
|
:current-page="searchPage"
|
||||||
:page-size="searchPageSize"
|
:page-size="searchPageSize"
|
||||||
:view-mode="noteViewMode"
|
:view-mode="noteViewMode"
|
||||||
@select-note="selectSearchResultNote"
|
@select-note="selectSearchResultNote"
|
||||||
|
@select-task-list="selectTaskList"
|
||||||
@page-change="setSearchPage"
|
@page-change="setSearchPage"
|
||||||
/>
|
/>
|
||||||
<NoteEditor
|
<NoteEditor
|
||||||
@@ -231,13 +253,14 @@
|
|||||||
:linked-tasks="linkedTasksForSelectedNote"
|
:linked-tasks="linkedTasksForSelectedNote"
|
||||||
@open-linked-task="openLinkedTaskFromNote"
|
@open-linked-task="openLinkedTaskFromNote"
|
||||||
/>
|
/>
|
||||||
<NoteList
|
<WorkspaceList
|
||||||
v-else
|
v-else
|
||||||
:notes="displayedNotes"
|
:items="displayedItems"
|
||||||
:can-load-more="canLoadMoreMainNotes"
|
:can-load-more="canLoadMoreMainNotes"
|
||||||
:is-loading-more="spaceStore.notesLoading"
|
:is-loading-more="spaceStore.notesLoading"
|
||||||
:view-mode="noteViewMode"
|
:view-mode="noteViewMode"
|
||||||
@select-note="selectNote"
|
@select-note="selectNote"
|
||||||
|
@select-task-list="selectTaskList"
|
||||||
@load-more="loadMoreMainNotes"
|
@load-more="loadMoreMainNotes"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -279,6 +302,13 @@
|
|||||||
@submit="submitCategory"
|
@submit="submitCategory"
|
||||||
/>
|
/>
|
||||||
<CreateNoteModal v-if="showCreateNoteModal" :category-options="categoryOptions" :default-category-id="selectedCategory?.id || null" @close="showCreateNoteModal = false" @create="createNote" />
|
<CreateNoteModal v-if="showCreateNoteModal" :category-options="categoryOptions" :default-category-id="selectedCategory?.id || null" @close="showCreateNoteModal = false" @create="createNote" />
|
||||||
|
<CreateTaskListModal
|
||||||
|
v-if="showCreateTaskListModal"
|
||||||
|
:category-options="categoryOptions"
|
||||||
|
:default-category-id="selectedCategory?.id || null"
|
||||||
|
@close="showCreateTaskListModal = false"
|
||||||
|
@create="createTaskList"
|
||||||
|
/>
|
||||||
<SpaceSettingsModal
|
<SpaceSettingsModal
|
||||||
v-if="showSpaceSettingsModal && currentSpace && canManageSpaceSettings"
|
v-if="showSpaceSettingsModal && currentSpace && canManageSpaceSettings"
|
||||||
:space="currentSpace"
|
:space="currentSpace"
|
||||||
@@ -290,7 +320,6 @@
|
|||||||
v-if="showTaskModal"
|
v-if="showTaskModal"
|
||||||
:task="taskModalDraft || {}"
|
:task="taskModalDraft || {}"
|
||||||
:statuses="taskStatuses"
|
:statuses="taskStatuses"
|
||||||
:category-options="categoryOptions"
|
|
||||||
:parent-task-options="taskParentOptions"
|
:parent-task-options="taskParentOptions"
|
||||||
:subtasks="taskDetail?.subtasks || []"
|
:subtasks="taskDetail?.subtasks || []"
|
||||||
@close="showTaskModal = false"
|
@close="showTaskModal = false"
|
||||||
@@ -342,11 +371,12 @@ import { useSpaceStore } from "./stores/spaceStore";
|
|||||||
import CategoryTree from "./components/CategoryTree.vue";
|
import CategoryTree from "./components/CategoryTree.vue";
|
||||||
import NoteEditor from "./components/NoteEditor.vue";
|
import NoteEditor from "./components/NoteEditor.vue";
|
||||||
import NoteViewer from "./components/NoteViewer.vue";
|
import NoteViewer from "./components/NoteViewer.vue";
|
||||||
import NoteList from "./components/NoteList.vue";
|
import WorkspaceList from "./components/WorkspaceList.vue";
|
||||||
import SearchResultsPage from "./components/SearchResultsPage.vue";
|
import SearchResultsPage from "./components/SearchResultsPage.vue";
|
||||||
import CreateSpaceModal from "./components/CreateSpaceModal.vue";
|
import CreateSpaceModal from "./components/CreateSpaceModal.vue";
|
||||||
import CreateCategoryModal from "./components/CreateCategoryModal.vue";
|
import CreateCategoryModal from "./components/CreateCategoryModal.vue";
|
||||||
import CreateNoteModal from "./components/CreateNoteModal.vue";
|
import CreateNoteModal from "./components/CreateNoteModal.vue";
|
||||||
|
import CreateTaskListModal from "./components/CreateTaskListModal.vue";
|
||||||
import SpaceSettingsModal from "./components/SpaceSettingsModal.vue";
|
import SpaceSettingsModal from "./components/SpaceSettingsModal.vue";
|
||||||
import TaskBoard from "./components/TaskBoard.vue";
|
import TaskBoard from "./components/TaskBoard.vue";
|
||||||
import TaskDetailModal from "./components/TaskDetailModal.vue";
|
import TaskDetailModal from "./components/TaskDetailModal.vue";
|
||||||
@@ -363,12 +393,14 @@ const showUserMenu = ref(false);
|
|||||||
const showCreateSpaceModal = ref(false);
|
const showCreateSpaceModal = ref(false);
|
||||||
const showCreateCategoryModal = ref(false);
|
const showCreateCategoryModal = ref(false);
|
||||||
const showCreateNoteModal = ref(false);
|
const showCreateNoteModal = ref(false);
|
||||||
|
const showCreateTaskListModal = ref(false);
|
||||||
const showSpaceSettingsModal = ref(false);
|
const showSpaceSettingsModal = ref(false);
|
||||||
const showSidebar = ref(false);
|
const showSidebar = ref(false);
|
||||||
const navbarRef = ref(null);
|
const navbarRef = ref(null);
|
||||||
const navbarHeight = ref(56);
|
const navbarHeight = ref(56);
|
||||||
const spaceDropdownRef = ref(null);
|
const spaceDropdownRef = ref(null);
|
||||||
const userDropdownRef = ref(null);
|
const userDropdownRef = ref(null);
|
||||||
|
const createDropdownRef = ref(null);
|
||||||
const searchQuery = ref("");
|
const searchQuery = ref("");
|
||||||
const selectedNote = ref(null);
|
const selectedNote = ref(null);
|
||||||
const selectedCategory = ref(null);
|
const selectedCategory = ref(null);
|
||||||
@@ -393,14 +425,16 @@ const unlockError = ref("");
|
|||||||
const unlockingNote = ref(false);
|
const unlockingNote = ref(false);
|
||||||
const activeView = ref("notes");
|
const activeView = ref("notes");
|
||||||
const taskFilters = ref({
|
const taskFilters = ref({
|
||||||
categoryId: null,
|
taskListId: null,
|
||||||
statusId: null,
|
statusId: null,
|
||||||
parentTaskId: null,
|
parentTaskId: null,
|
||||||
});
|
});
|
||||||
|
const selectedTaskList = ref(null);
|
||||||
const showTaskModal = ref(false);
|
const showTaskModal = ref(false);
|
||||||
const taskDetail = ref(null);
|
const taskDetail = ref(null);
|
||||||
const taskModalDraft = ref(null);
|
const taskModalDraft = ref(null);
|
||||||
const linkedTasksForSelectedNote = ref([]);
|
const linkedTasksForSelectedNote = ref([]);
|
||||||
|
const showCreateMenu = ref(false);
|
||||||
|
|
||||||
const currentUser = computed(() => authStore.user);
|
const currentUser = computed(() => authStore.user);
|
||||||
const isAdminRoute = computed(() => route.path === "/admin");
|
const isAdminRoute = computed(() => route.path === "/admin");
|
||||||
@@ -410,6 +444,19 @@ const isAuthRoute = computed(() => route.path === "/login" || route.path === "/r
|
|||||||
const spaces = computed(() => spaceStore.spaces);
|
const spaces = computed(() => spaceStore.spaces);
|
||||||
const currentSpace = computed(() => spaceStore.currentSpace);
|
const currentSpace = computed(() => spaceStore.currentSpace);
|
||||||
const searchResults = computed(() => sortNotesByPriority(spaceStore.searchResults));
|
const searchResults = computed(() => sortNotesByPriority(spaceStore.searchResults));
|
||||||
|
const searchItems = computed(() => {
|
||||||
|
const query = searchQuery.value.toLowerCase();
|
||||||
|
const noteItems = searchResults.value.map((note) => ({
|
||||||
|
...note,
|
||||||
|
kind: "note",
|
||||||
|
}));
|
||||||
|
const matchingTaskLists = (spaceStore.taskLists || []).filter((tl) => tl.name.toLowerCase().includes(query));
|
||||||
|
const taskListItems = matchingTaskLists.map((tl) => ({
|
||||||
|
...tl,
|
||||||
|
kind: "task-list",
|
||||||
|
}));
|
||||||
|
return [...taskListItems, ...noteItems];
|
||||||
|
});
|
||||||
const searchPageSize = 12;
|
const searchPageSize = 12;
|
||||||
const searchPage = computed(() => {
|
const searchPage = computed(() => {
|
||||||
const pageValue = Number.parseInt(route.query.page || "1", 10);
|
const pageValue = Number.parseInt(route.query.page || "1", 10);
|
||||||
@@ -469,13 +516,46 @@ const collectNotesFromCategory = (category, bucket = []) => {
|
|||||||
return bucket;
|
return bucket;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const collectTaskListsFromCategory = (category, bucket = []) => {
|
||||||
|
if (!category) {
|
||||||
|
return bucket;
|
||||||
|
}
|
||||||
|
if (Array.isArray(category.task_lists) && category.task_lists.length) {
|
||||||
|
bucket.push(...category.task_lists);
|
||||||
|
}
|
||||||
|
for (const subcategory of category.subcategories || []) {
|
||||||
|
collectTaskListsFromCategory(subcategory, bucket);
|
||||||
|
}
|
||||||
|
return bucket;
|
||||||
|
};
|
||||||
|
|
||||||
const displayedNotes = computed(() => {
|
const displayedNotes = computed(() => {
|
||||||
if (!selectedCategory.value) {
|
if (!selectedCategory.value) {
|
||||||
return sortNotesByPriority(spaceStore.notes);
|
return sortNotesByPriority(spaceStore.notes);
|
||||||
}
|
}
|
||||||
return sortNotesByPriority(collectNotesFromCategory(selectedCategory.value, []));
|
return sortNotesByPriority(collectNotesFromCategory(selectedCategory.value, []));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const displayedTaskLists = computed(() => {
|
||||||
|
if (!selectedCategory.value) {
|
||||||
|
return [...(spaceStore.taskLists || [])].sort((left, right) => left.name.localeCompare(right.name));
|
||||||
|
}
|
||||||
|
return [...collectTaskListsFromCategory(selectedCategory.value, [])].sort((left, right) => left.name.localeCompare(right.name));
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayedItems = computed(() => {
|
||||||
|
const taskListItems = displayedTaskLists.value.map((taskList) => ({
|
||||||
|
...taskList,
|
||||||
|
kind: "task-list",
|
||||||
|
}));
|
||||||
|
const noteItems = displayedNotes.value.map((note) => ({
|
||||||
|
...note,
|
||||||
|
kind: "note",
|
||||||
|
}));
|
||||||
|
return [...taskListItems, ...noteItems];
|
||||||
|
});
|
||||||
const tasks = computed(() => spaceStore.tasks || []);
|
const tasks = computed(() => spaceStore.tasks || []);
|
||||||
|
const taskLists = computed(() => spaceStore.taskLists || []);
|
||||||
const taskStatuses = computed(() => spaceStore.taskStatuses || []);
|
const taskStatuses = computed(() => spaceStore.taskStatuses || []);
|
||||||
const initialTaskStatusId = computed(() => {
|
const initialTaskStatusId = computed(() => {
|
||||||
if (!taskStatuses.value.length) {
|
if (!taskStatuses.value.length) {
|
||||||
@@ -518,6 +598,13 @@ const openSpaceHome = () => {
|
|||||||
unlockPassword.value = "";
|
unlockPassword.value = "";
|
||||||
unlockError.value = "";
|
unlockError.value = "";
|
||||||
searchQuery.value = "";
|
searchQuery.value = "";
|
||||||
|
selectedTaskList.value = null;
|
||||||
|
activeView.value = "notes";
|
||||||
|
taskFilters.value = {
|
||||||
|
taskListId: null,
|
||||||
|
statusId: null,
|
||||||
|
parentTaskId: null,
|
||||||
|
};
|
||||||
spaceStore.clearSearchResults();
|
spaceStore.clearSearchResults();
|
||||||
if (route.path !== "/") {
|
if (route.path !== "/") {
|
||||||
router.push("/");
|
router.push("/");
|
||||||
@@ -580,6 +667,27 @@ const breadcrumbItems = computed(() => {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (activeView.value === "tasks" && selectedTaskList.value) {
|
||||||
|
const taskListCategoryId = selectedTaskList.value.category_id;
|
||||||
|
if (taskListCategoryId) {
|
||||||
|
const categoryTrail = findCategoryPath(categoryTree.value, taskListCategoryId) || [];
|
||||||
|
for (const category of categoryTrail) {
|
||||||
|
items.push({
|
||||||
|
label: category.name,
|
||||||
|
clickable: true,
|
||||||
|
onClick: () => openCategoryFromBreadcrumb(category),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
label: selectedTaskList.value.name,
|
||||||
|
clickable: false,
|
||||||
|
onClick: null,
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedCategory.value) {
|
if (selectedCategory.value) {
|
||||||
const categoryTrail = findCategoryPath(categoryTree.value, selectedCategory.value.id) || [selectedCategory.value];
|
const categoryTrail = findCategoryPath(categoryTree.value, selectedCategory.value.id) || [selectedCategory.value];
|
||||||
for (let i = 0; i < categoryTrail.length; i++) {
|
for (let i = 0; i < categoryTrail.length; i++) {
|
||||||
@@ -710,6 +818,14 @@ const toggleUserMenu = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleCreateMenu = () => {
|
||||||
|
showCreateMenu.value = !showCreateMenu.value;
|
||||||
|
if (showCreateMenu.value) {
|
||||||
|
showSpaceDropdown.value = false;
|
||||||
|
showUserMenu.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDocumentClick = (event) => {
|
const handleDocumentClick = (event) => {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
|
|
||||||
@@ -720,6 +836,10 @@ const handleDocumentClick = (event) => {
|
|||||||
if (showUserMenu.value && userDropdownRef.value && !userDropdownRef.value.contains(target)) {
|
if (showUserMenu.value && userDropdownRef.value && !userDropdownRef.value.contains(target)) {
|
||||||
showUserMenu.value = false;
|
showUserMenu.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showCreateMenu.value && createDropdownRef.value && !createDropdownRef.value.contains(target)) {
|
||||||
|
showCreateMenu.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEscapeKey = (event) => {
|
const handleEscapeKey = (event) => {
|
||||||
@@ -728,6 +848,7 @@ const handleEscapeKey = (event) => {
|
|||||||
}
|
}
|
||||||
showSpaceDropdown.value = false;
|
showSpaceDropdown.value = false;
|
||||||
showUserMenu.value = false;
|
showUserMenu.value = false;
|
||||||
|
showCreateMenu.value = false;
|
||||||
showSidebar.value = false;
|
showSidebar.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -737,7 +858,9 @@ const selectSpace = async (space) => {
|
|||||||
localStorage.setItem("currentSpaceId", space.id);
|
localStorage.setItem("currentSpaceId", space.id);
|
||||||
selectedNote.value = null;
|
selectedNote.value = null;
|
||||||
selectedCategory.value = null;
|
selectedCategory.value = null;
|
||||||
|
selectedTaskList.value = null;
|
||||||
isEditingNote.value = false;
|
isEditingNote.value = false;
|
||||||
|
activeView.value = "notes";
|
||||||
linkedTasksForSelectedNote.value = [];
|
linkedTasksForSelectedNote.value = [];
|
||||||
await applyTaskFilters(taskFilters.value);
|
await applyTaskFilters(taskFilters.value);
|
||||||
};
|
};
|
||||||
@@ -782,8 +905,13 @@ const selectNote = async (note) => {
|
|||||||
showUnlockModal.value = true;
|
showUnlockModal.value = true;
|
||||||
selectedNote.value = null;
|
selectedNote.value = null;
|
||||||
selectedCategory.value = null;
|
selectedCategory.value = null;
|
||||||
|
selectedTaskList.value = null;
|
||||||
isEditingNote.value = false;
|
isEditingNote.value = false;
|
||||||
|
activeView.value = "notes";
|
||||||
showSidebar.value = false;
|
showSidebar.value = false;
|
||||||
|
if (route.path === "/search") {
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,8 +923,13 @@ const selectNote = async (note) => {
|
|||||||
const response = await apiClient.get(`/api/v1/spaces/${currentSpace.value.id}/notes/${note.id}`);
|
const response = await apiClient.get(`/api/v1/spaces/${currentSpace.value.id}/notes/${note.id}`);
|
||||||
selectedNote.value = response.data;
|
selectedNote.value = response.data;
|
||||||
selectedCategory.value = null;
|
selectedCategory.value = null;
|
||||||
|
selectedTaskList.value = null;
|
||||||
isEditingNote.value = false;
|
isEditingNote.value = false;
|
||||||
|
activeView.value = "notes";
|
||||||
showSidebar.value = false;
|
showSidebar.value = false;
|
||||||
|
if (route.path === "/search") {
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
alert("Unable to load note content.");
|
alert("Unable to load note content.");
|
||||||
}
|
}
|
||||||
@@ -828,7 +961,9 @@ const unlockProtectedNote = async () => {
|
|||||||
});
|
});
|
||||||
selectedNote.value = response.data;
|
selectedNote.value = response.data;
|
||||||
selectedCategory.value = null;
|
selectedCategory.value = null;
|
||||||
|
selectedTaskList.value = null;
|
||||||
isEditingNote.value = false;
|
isEditingNote.value = false;
|
||||||
|
activeView.value = "notes";
|
||||||
showSidebar.value = false;
|
showSidebar.value = false;
|
||||||
closeUnlockModal();
|
closeUnlockModal();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -840,11 +975,31 @@ const unlockProtectedNote = async () => {
|
|||||||
|
|
||||||
const selectCategory = (category) => {
|
const selectCategory = (category) => {
|
||||||
selectedCategory.value = category;
|
selectedCategory.value = category;
|
||||||
|
selectedTaskList.value = null;
|
||||||
selectedNote.value = null;
|
selectedNote.value = null;
|
||||||
isEditingNote.value = false;
|
isEditingNote.value = false;
|
||||||
|
activeView.value = "notes";
|
||||||
showSidebar.value = false;
|
showSidebar.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectTaskList = async (taskList) => {
|
||||||
|
selectedTaskList.value = taskList;
|
||||||
|
selectedCategory.value = null;
|
||||||
|
selectedNote.value = null;
|
||||||
|
isEditingNote.value = false;
|
||||||
|
activeView.value = "tasks";
|
||||||
|
showSidebar.value = false;
|
||||||
|
|
||||||
|
if (route.path === "/search") {
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
await applyTaskFilters({
|
||||||
|
...taskFilters.value,
|
||||||
|
taskListId: taskList?.id || null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const performSearch = async () => {
|
const performSearch = async () => {
|
||||||
const q = searchQuery.value.trim();
|
const q = searchQuery.value.trim();
|
||||||
if (!q) {
|
if (!q) {
|
||||||
@@ -903,22 +1058,30 @@ const loadMoreMainNotes = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const applyTaskFilters = async (filters) => {
|
const applyTaskFilters = async (filters) => {
|
||||||
taskFilters.value = filters;
|
taskFilters.value = {
|
||||||
|
...taskFilters.value,
|
||||||
|
...filters,
|
||||||
|
taskListId: selectedTaskList.value?.id || taskFilters.value.taskListId || null,
|
||||||
|
};
|
||||||
if (!currentSpace.value?.id) {
|
if (!currentSpace.value?.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await spaceStore.fetchTasks(currentSpace.value.id, filters);
|
await spaceStore.fetchTasks(currentSpace.value.id, taskFilters.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openTaskCreateModal = () => {
|
const openTaskCreateModal = () => {
|
||||||
if (!canCreateTasks.value) {
|
if (!canCreateTasks.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!selectedTaskList.value?.id) {
|
||||||
|
alert("Select a task list first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
taskDetail.value = null;
|
taskDetail.value = null;
|
||||||
taskModalDraft.value = {
|
taskModalDraft.value = {
|
||||||
title: "",
|
title: "",
|
||||||
description: "",
|
description: "",
|
||||||
category_id: selectedCategory.value?.id || null,
|
task_list_id: selectedTaskList.value?.id || null,
|
||||||
status_id: initialTaskStatusId.value,
|
status_id: initialTaskStatusId.value,
|
||||||
parent_task_id: null,
|
parent_task_id: null,
|
||||||
note_links: selectedNote.value?.id ? [selectedNote.value.id] : [],
|
note_links: selectedNote.value?.id ? [selectedNote.value.id] : [],
|
||||||
@@ -1010,7 +1173,7 @@ const createSubtask = (parentTask) => {
|
|||||||
taskModalDraft.value = {
|
taskModalDraft.value = {
|
||||||
title: "",
|
title: "",
|
||||||
description: "",
|
description: "",
|
||||||
category_id: parentTask.category_id || null,
|
task_list_id: parentTask.task_list_id || selectedTaskList.value?.id || null,
|
||||||
status_id: initialTaskStatusId.value,
|
status_id: initialTaskStatusId.value,
|
||||||
parent_task_id: parentTask.id,
|
parent_task_id: parentTask.id,
|
||||||
note_links: selectedNote.value?.id ? [selectedNote.value.id] : [],
|
note_links: selectedNote.value?.id ? [selectedNote.value.id] : [],
|
||||||
@@ -1102,10 +1265,68 @@ const updateTaskStatusFromBoard = async ({ taskId, currentStatusId, targetStatus
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openLinkedTaskFromNote = async (task) => {
|
const openLinkedTaskFromNote = async (task) => {
|
||||||
|
selectedTaskList.value = taskLists.value.find((list) => list.id === task.task_list_id) || null;
|
||||||
|
selectedNote.value = null;
|
||||||
|
await applyTaskFilters({ taskListId: selectedTaskList.value?.id || null });
|
||||||
activeView.value = "tasks";
|
activeView.value = "tasks";
|
||||||
await openTaskDetail(task);
|
await openTaskDetail(task);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openCreateNoteModalFromMenu = () => {
|
||||||
|
showCreateMenu.value = false;
|
||||||
|
showCreateNoteModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateTaskListModalFromMenu = () => {
|
||||||
|
showCreateMenu.value = false;
|
||||||
|
showCreateTaskListModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTaskList = async (taskListData) => {
|
||||||
|
if (!currentSpace.value?.id || !canCreateTasks.value) {
|
||||||
|
showCreateTaskListModal.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const created = await spaceStore.createTaskList(currentSpace.value.id, taskListData);
|
||||||
|
showCreateTaskListModal.value = false;
|
||||||
|
await selectTaskList(created);
|
||||||
|
} catch (error) {
|
||||||
|
alert(error?.response?.data || "Unable to create task list.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTaskList = async (taskList) => {
|
||||||
|
if (!currentSpace.value?.id || !taskList?.id || !canDeleteTasks.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm(`Delete task list "${taskList.name}" and all associated tasks?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await spaceStore.deleteTaskList(currentSpace.value.id, taskList.id);
|
||||||
|
|
||||||
|
if (selectedTaskList.value?.id === taskList.id) {
|
||||||
|
selectedTaskList.value = null;
|
||||||
|
taskDetail.value = null;
|
||||||
|
taskModalDraft.value = null;
|
||||||
|
showTaskModal.value = false;
|
||||||
|
taskFilters.value = {
|
||||||
|
taskListId: null,
|
||||||
|
statusId: null,
|
||||||
|
parentTaskId: null,
|
||||||
|
};
|
||||||
|
await spaceStore.fetchTasks(currentSpace.value.id, taskFilters.value);
|
||||||
|
activeView.value = "notes";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert(error?.response?.data || "Unable to delete task list.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const createSpace = async (spaceData) => {
|
const createSpace = async (spaceData) => {
|
||||||
showCreateSpaceModal.value = false;
|
showCreateSpaceModal.value = false;
|
||||||
await spaceStore.createSpace(spaceData);
|
await spaceStore.createSpace(spaceData);
|
||||||
@@ -1258,257 +1479,4 @@ const logout = () => {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="./assets/styles/scoped/App.css"></style>
|
||||||
.app-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar {
|
|
||||||
z-index: 1100;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-navbar {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-left {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-controls {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-brand {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-main {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
width: 280px;
|
|
||||||
overflow-y: auto;
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-content {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header {
|
|
||||||
border-bottom: 1px solid #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb-title {
|
|
||||||
font-size: 1rem;
|
|
||||||
color: #495057;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb-link {
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
padding: 0;
|
|
||||||
color: #0d6efd;
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb-link:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb-separator {
|
|
||||||
color: #6c757d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-box {
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu {
|
|
||||||
z-index: 1200;
|
|
||||||
max-width: min(92vw, 320px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu-end {
|
|
||||||
right: 0;
|
|
||||||
left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu.show {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-route-view {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.app-navbar {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
grid-template-areas:
|
|
||||||
"left user"
|
|
||||||
"space space"
|
|
||||||
"search search";
|
|
||||||
row-gap: 0.5rem;
|
|
||||||
column-gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-left {
|
|
||||||
grid-area: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-controls {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-user-menu {
|
|
||||||
grid-area: user;
|
|
||||||
justify-self: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-space-selector {
|
|
||||||
grid-area: space;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-space-selector > .btn {
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-search {
|
|
||||||
grid-area: search;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-search .form-control {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-box {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-brand {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu-toggle {
|
|
||||||
padding: 0.35rem 0.55rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-backdrop {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
z-index: 1090;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 280px;
|
|
||||||
z-index: 1095;
|
|
||||||
transform: translateX(-100%);
|
|
||||||
transition: transform 0.3s ease-in-out;
|
|
||||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar.open {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
position: relative;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-label {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button .mdi {
|
|
||||||
margin-right: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.col-auto .action-button {
|
|
||||||
min-width: 2.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-main {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode overrides */
|
|
||||||
:root[data-bs-theme="dark"] .sidebar-header {
|
|
||||||
border-bottom-color: #3a3f4b;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .breadcrumb-title {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .breadcrumb-link {
|
|
||||||
color: #7aa2f7;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .breadcrumb-separator {
|
|
||||||
color: #4a5568;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,42 +1,70 @@
|
|||||||
:root {
|
:root {
|
||||||
--primary-color: #667eea;
|
--color-primary: #667eea;
|
||||||
--secondary-color: #764ba2;
|
--color-primary-strong: #4f46a5;
|
||||||
--text-color: #333;
|
--color-text: #333333;
|
||||||
--bg-color: #f8f9fa;
|
--color-text-muted: #6c757d;
|
||||||
--border-color: #dee2e6;
|
--color-bg: #f8f9fa;
|
||||||
|
--color-surface: #ffffff;
|
||||||
|
--color-surface-muted: #f1f3f5;
|
||||||
|
--color-border: #dee2e6;
|
||||||
|
--color-info: #748ffc;
|
||||||
|
--color-code-bg: #353943;
|
||||||
|
--color-code-text: #f9fafb;
|
||||||
|
--color-scroll-track: #f1f1f1;
|
||||||
|
--color-scroll-thumb: #888888;
|
||||||
|
--color-scroll-thumb-hover: #555555;
|
||||||
|
|
||||||
|
--primary-color: var(--color-primary);
|
||||||
|
--secondary-color: var(--color-primary-strong);
|
||||||
|
--text-color: var(--color-text);
|
||||||
|
--bg-color: var(--color-bg);
|
||||||
|
--border-color: var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme="dark"] {
|
[data-bs-theme="dark"] {
|
||||||
--text-color: #e2e8f0;
|
--color-text: #e2e8f0;
|
||||||
--bg-color: #1a1d23;
|
--color-text-muted: #94a3b8;
|
||||||
--border-color: #3a3f4b;
|
--color-bg: #1a1d23;
|
||||||
|
--color-surface: #21252e;
|
||||||
|
--color-surface-muted: #2d3748;
|
||||||
|
--color-border: #3a3f4b;
|
||||||
|
--color-info: #7aa2f7;
|
||||||
|
--color-code-bg: #2d3748;
|
||||||
|
--color-code-text: #e2e8f0;
|
||||||
|
--color-scroll-track: #2d3748;
|
||||||
|
--color-scroll-thumb: #4a5568;
|
||||||
|
--color-scroll-thumb-hover: #718096;
|
||||||
|
|
||||||
|
--text-color: var(--color-text);
|
||||||
|
--bg-color: var(--color-bg);
|
||||||
|
--border-color: var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme="dark"] body {
|
[data-bs-theme="dark"] body {
|
||||||
background-color: #1a1d23;
|
background-color: var(--color-bg);
|
||||||
color: #e2e8f0;
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme="dark"] .sidebar {
|
[data-bs-theme="dark"] .sidebar {
|
||||||
background-color: #21252e !important;
|
background-color: var(--color-surface) !important;
|
||||||
border-color: #3a3f4b !important;
|
border-color: var(--color-border) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme="dark"] .toolbar {
|
[data-bs-theme="dark"] .toolbar {
|
||||||
background-color: #21252e;
|
background-color: var(--color-surface);
|
||||||
border-color: #3a3f4b !important;
|
border-color: var(--color-border) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme="dark"] .main-content {
|
[data-bs-theme="dark"] .main-content {
|
||||||
background-color: #1a1d23;
|
background-color: var(--color-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme="dark"] .markdown-body table {
|
[data-bs-theme="dark"] .markdown-body table {
|
||||||
background: #21252e;
|
background: var(--color-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme="dark"] .markdown-body th {
|
[data-bs-theme="dark"] .markdown-body th {
|
||||||
background: #2a2f3a;
|
background: var(--color-surface-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme="dark"] .markdown-body tr:nth-child(even) td {
|
[data-bs-theme="dark"] .markdown-body tr:nth-child(even) td {
|
||||||
@@ -49,8 +77,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme="dark"] .markdown-body :not(pre) > code {
|
[data-bs-theme="dark"] .markdown-body :not(pre) > code {
|
||||||
background: #2d3748;
|
background: var(--color-surface-muted);
|
||||||
color: #e2e8f0;
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme="dark"] .markdown-body pre code {
|
[data-bs-theme="dark"] .markdown-body pre code {
|
||||||
@@ -59,20 +87,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme="dark"] .markdown-body pre {
|
[data-bs-theme="dark"] .markdown-body pre {
|
||||||
background: #2d3748;
|
background: var(--color-code-bg);
|
||||||
color: #e2e8f0;
|
color: var(--color-code-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme="dark"] ::-webkit-scrollbar-track {
|
[data-bs-theme="dark"] ::-webkit-scrollbar-track {
|
||||||
background: #2d3748;
|
background: var(--color-scroll-track);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme="dark"] ::-webkit-scrollbar-thumb {
|
[data-bs-theme="dark"] ::-webkit-scrollbar-thumb {
|
||||||
background: #4a5568;
|
background: var(--color-scroll-thumb);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme="dark"] ::-webkit-scrollbar-thumb:hover {
|
[data-bs-theme="dark"] ::-webkit-scrollbar-thumb:hover {
|
||||||
background: #718096;
|
background: var(--color-scroll-thumb-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -99,7 +127,7 @@ body,
|
|||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
background: #fff;
|
background: var(--color-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body th,
|
.markdown-body th,
|
||||||
@@ -126,7 +154,7 @@ body,
|
|||||||
.markdown-body blockquote {
|
.markdown-body blockquote {
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
border-left: 4px solid #748ffc;
|
border-left: 4px solid var(--color-info);
|
||||||
background: #f8f9ff;
|
background: #f8f9ff;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
}
|
}
|
||||||
@@ -139,8 +167,8 @@ body,
|
|||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
background: #353943;
|
background: var(--color-code-bg);
|
||||||
color: #f9fafb;
|
color: var(--color-code-text);
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +183,7 @@ body,
|
|||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
padding: 0.1rem 0.3rem;
|
padding: 0.1rem 0.3rem;
|
||||||
border-radius: 0.35rem;
|
border-radius: 0.35rem;
|
||||||
background: #f1f3f5;
|
background: var(--color-surface-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scrollbar styling */
|
/* Scrollbar styling */
|
||||||
@@ -165,14 +193,14 @@ body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #f1f1f1;
|
background: var(--color-scroll-track);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #888;
|
background: var(--color-scroll-thumb);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #555;
|
background: var(--color-scroll-thumb-hover);
|
||||||
}
|
}
|
||||||
|
|||||||
254
frontend/src/assets/styles/scoped/App.css
Normal file
254
frontend/src/assets/styles/scoped/App.css
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
.app-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
z-index: 1100;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-navbar {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-left {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-controls {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-brand {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-main {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 280px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #495057;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-link {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-separator {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
z-index: 1200;
|
||||||
|
max-width: min(92vw, 320px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu-end {
|
||||||
|
right: 0;
|
||||||
|
left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-route-view {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-navbar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"left user"
|
||||||
|
"space space"
|
||||||
|
"search search";
|
||||||
|
row-gap: 0.5rem;
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-left {
|
||||||
|
grid-area: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-controls {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-user-menu {
|
||||||
|
grid-area: user;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-space-selector {
|
||||||
|
grid-area: space;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-space-selector > .btn {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-search {
|
||||||
|
grid-area: search;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-search .form-control {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-brand {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu-toggle {
|
||||||
|
padding: 0.35rem 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 1090;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 280px;
|
||||||
|
z-index: 1095;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button .mdi {
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-auto .action-button {
|
||||||
|
min-width: 2.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-main {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode overrides */
|
||||||
|
:root[data-bs-theme="dark"] .sidebar-header {
|
||||||
|
border-bottom-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .breadcrumb-title {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .breadcrumb-link {
|
||||||
|
color: #7aa2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .breadcrumb-separator {
|
||||||
|
color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
@import "./AdminModal.shared.css";
|
||||||
|
|
||||||
|
.permissions-textarea {
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
.admin-modal {
|
||||||
|
z-index: 2000;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-top: max(0.5rem, env(safe-area-inset-top));
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-modal-backdrop {
|
||||||
|
z-index: 1990;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-modal .modal-dialog {
|
||||||
|
margin: 1rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.admin-modal {
|
||||||
|
padding-top: max(0.75rem, env(safe-area-inset-top));
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-modal .modal-dialog {
|
||||||
|
margin: 0.75rem;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "./AdminModal.shared.css";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
@import "./AdminModal.shared.css";
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "./AdminModal.shared.css";
|
||||||
213
frontend/src/assets/styles/scoped/components/CategoryTree.css
Normal file
213
frontend/src/assets/styles/scoped/components/CategoryTree.css
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
.category-item {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-header:hover {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon {
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-name {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-actions {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-button {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-button:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 30px;
|
||||||
|
right: 0;
|
||||||
|
min-width: 150px;
|
||||||
|
padding: 0.35rem;
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 10px 25px rgba(15, 23, 42, 0.15);
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.45rem 0.6rem;
|
||||||
|
border-radius: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background-color: #f1f3f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.danger {
|
||||||
|
color: #c92a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-content {
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
color: #2f3d52;
|
||||||
|
background: #f7f9ff;
|
||||||
|
border: 1px solid #d9e3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-list-item:hover {
|
||||||
|
background: #e9efff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-list-item span {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-item:hover {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-item span {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-icon {
|
||||||
|
color: #408aca;
|
||||||
|
font-size: 0.9em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-icon {
|
||||||
|
color: #f08c00;
|
||||||
|
font-size: 0.9em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-item.is-pinned {
|
||||||
|
background: #dbf5ff;
|
||||||
|
border: 1px solid #a8d1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-item.is-pinned:hover {
|
||||||
|
background: #c5e9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-item.is-featured {
|
||||||
|
background: #fff9db;
|
||||||
|
border: 1px solid #ffd8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-item.is-featured:hover {
|
||||||
|
background: #fff3c5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subcategories {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode overrides */
|
||||||
|
:root[data-bs-theme="dark"] .category-header:hover {
|
||||||
|
background-color: var(--color-surface-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .menu-button {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .menu-button:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .menu-dropdown {
|
||||||
|
background: var(--color-surface-muted);
|
||||||
|
border-color: #4a5568;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .menu-item {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .menu-item:hover {
|
||||||
|
background-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .note-item:hover {
|
||||||
|
background-color: var(--color-surface-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .task-list-item {
|
||||||
|
background: #1f2a44;
|
||||||
|
border-color: #334b7d;
|
||||||
|
color: #bfceef;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .task-list-item:hover {
|
||||||
|
background: #26365b;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .note-item.is-pinned {
|
||||||
|
background: #1a3a5c;
|
||||||
|
border-color: #2d6a9f;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .note-item.is-pinned:hover {
|
||||||
|
background: #1e4470;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .note-item.is-featured {
|
||||||
|
background: #3a2e0a;
|
||||||
|
border-color: #7a5a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .note-item.is-featured:hover {
|
||||||
|
background: #453710;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.note-flags {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-check {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
.file-explorer {
|
||||||
|
background: var(--color-surface);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-explorer-header {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list {
|
||||||
|
max-height: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.1s;
|
||||||
|
color: var(--color-text);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover {
|
||||||
|
background-color: #f0f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-active {
|
||||||
|
outline: 2px dashed var(--color-primary);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover .btn-delete {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode overrides */
|
||||||
|
:root[data-bs-theme="dark"] .file-explorer {
|
||||||
|
background: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .file-explorer-header {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .file-item {
|
||||||
|
border-bottom-color: var(--color-border);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .file-item:hover {
|
||||||
|
background-color: var(--color-surface-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
.modal-backdrop-custom {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(15, 23, 42, 0.45);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 2000;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-panel {
|
||||||
|
width: min(920px, 100%);
|
||||||
|
max-height: min(92vh, 980px);
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid #dbe3ee;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.2);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
background: linear-gradient(180deg, #f8fafc 0%, var(--color-surface) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-modal-title {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-modal-close {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-modal-body {
|
||||||
|
padding: 1rem 1.25rem 1.25rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-section {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-list {
|
||||||
|
max-height: 220px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.modal-backdrop-custom {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-modal-header,
|
||||||
|
.provider-modal-body {
|
||||||
|
padding-left: 0.85rem;
|
||||||
|
padding-right: 0.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode overrides */
|
||||||
|
:root[data-bs-theme="dark"] .modal-panel {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .provider-modal-header {
|
||||||
|
background: linear-gradient(180deg, #2a2f3a 0%, var(--color-surface) 100%);
|
||||||
|
border-bottom-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .provider-section {
|
||||||
|
background: #2a2f3a;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
300
frontend/src/assets/styles/scoped/components/NoteEditor.css
Normal file
300
frontend/src/assets/styles/scoped/components/NoteEditor.css
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
.editor-toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-status.dirty {
|
||||||
|
color: #b26a00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-status.saving {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-status.saved {
|
||||||
|
color: #2b8a3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-textarea {
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
min-height: 600px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-flags {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-check {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
padding: 0.45rem 0.75rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--color-bg);
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-check-input {
|
||||||
|
margin: 0;
|
||||||
|
width: 1.1rem;
|
||||||
|
height: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-check-label {
|
||||||
|
line-height: 1;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-pane {
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-zone {
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #f3b5b5;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background: var(--color-surface)5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-zone-title {
|
||||||
|
color: #9f1c1c;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-zone-copy {
|
||||||
|
color: #7a2727;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-mention-panel {
|
||||||
|
margin-top: 0.45rem;
|
||||||
|
border: 1px solid #dbe4f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fbfdff;
|
||||||
|
padding: 0.5rem;
|
||||||
|
max-height: 220px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-mention-option {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0.35rem 0.45rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-mention-option:hover {
|
||||||
|
background: #eef3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-picker {
|
||||||
|
background: var(--color-surface);
|
||||||
|
min-height: 300px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-picker-header {
|
||||||
|
background: var(--color-bg);
|
||||||
|
min-height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-picker-search {
|
||||||
|
background: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-picker-list {
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 520px;
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-picker-item {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.35rem 0.45rem;
|
||||||
|
text-align: left;
|
||||||
|
gap: 0.4rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-picker-item:hover {
|
||||||
|
background: #eef3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-picker-title {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-picker-empty {
|
||||||
|
padding: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-picker-item small {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-picker .btn-link {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-picker-item {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(.task-inline-link) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.18rem 0.5rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #c7d8ff;
|
||||||
|
background: #eef4ff;
|
||||||
|
color: #2c4ea3;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(.task-inline-title) {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(.task-inline-status) {
|
||||||
|
line-height: 1;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.16rem 0.42rem;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--task-status-color, #5c7bd9) 60%, var(--color-surface) 40%);
|
||||||
|
background: color-mix(in srgb, var(--task-status-color, #5c7bd9) 18%, var(--color-surface) 82%);
|
||||||
|
color: color-mix(in srgb, var(--task-status-color, #5c7bd9) 72%, #0f172a 28%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(.task-inline-link:hover) {
|
||||||
|
background: #dfeaff;
|
||||||
|
border-color: #aac4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(.task-inline-link i) {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode overrides */
|
||||||
|
:root[data-bs-theme="dark"] .editor-toolbar {
|
||||||
|
border-bottom-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .flag-check {
|
||||||
|
background: var(--color-surface-muted);
|
||||||
|
border-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .preview-pane {
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .danger-zone {
|
||||||
|
background: #2d1a1a;
|
||||||
|
border-color: #7a3030;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .danger-zone-title {
|
||||||
|
color: #fc8181;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .danger-zone-copy {
|
||||||
|
color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .task-mention-panel {
|
||||||
|
border-color: #3a4558;
|
||||||
|
background: #1f2733;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .task-mention-option:hover {
|
||||||
|
background: #2b3646;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .task-picker {
|
||||||
|
border-color: var(--color-border) !important;
|
||||||
|
background: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .task-picker-header {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-bottom-color: var(--color-border) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .task-picker-search {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-bottom-color: var(--color-border) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .task-picker-search .form-control {
|
||||||
|
background: #1f2430;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .task-picker-item {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .task-picker-item:hover {
|
||||||
|
background: var(--color-surface-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .task-picker-item small {
|
||||||
|
color: #a8b4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .markdown-body :deep(.task-inline-link) {
|
||||||
|
border-color: #35508b;
|
||||||
|
background: #1b2a4a;
|
||||||
|
color: #9ec0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .markdown-body :deep(.task-inline-link:hover) {
|
||||||
|
background: #22345c;
|
||||||
|
border-color: #4566ad;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .markdown-body :deep(.task-inline-status) {
|
||||||
|
border-color: color-mix(in srgb, var(--task-status-color, #7aa2f7) 65%, #1e293b 35%);
|
||||||
|
background: color-mix(in srgb, var(--task-status-color, #7aa2f7) 26%, #111827 74%);
|
||||||
|
color: color-mix(in srgb, var(--task-status-color, #7aa2f7) 70%, #dbeafe 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
168
frontend/src/assets/styles/scoped/components/NoteViewer.css
Normal file
168
frontend/src/assets/styles/scoped/components/NoteViewer.css
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
.note-viewer {
|
||||||
|
max-width: 900px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-meta {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-grid {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.2rem 0.55rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eef2ff;
|
||||||
|
color: #364fc7;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.25rem 0.6rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pinned-chip {
|
||||||
|
color: #005f8f;
|
||||||
|
background: #dbf5ff;
|
||||||
|
border: 1px solid #a8d1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-chip {
|
||||||
|
color: #8d7619;
|
||||||
|
border: 1px solid #ffd8a8;
|
||||||
|
background: #fff9db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-chip {
|
||||||
|
color: #0c5460;
|
||||||
|
border: 1px solid #a5d8ff;
|
||||||
|
background: #e7f5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.private-chip {
|
||||||
|
color: #5f3dc4;
|
||||||
|
border: 1px solid #d0bfff;
|
||||||
|
background: #f3f0ff;
|
||||||
|
}
|
||||||
|
.protected-chip {
|
||||||
|
color: #7f5539;
|
||||||
|
border: 1px solid #e0c3a6;
|
||||||
|
background: #fff4e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(.task-inline-link) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.18rem 0.5rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #c7d8ff;
|
||||||
|
background: #eef4ff;
|
||||||
|
color: #2c4ea3;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(.task-inline-title) {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(.task-inline-status) {
|
||||||
|
line-height: 1;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.16rem 0.42rem;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--task-status-color, #5c7bd9) 60%, var(--color-surface) 40%);
|
||||||
|
background: color-mix(in srgb, var(--task-status-color, #5c7bd9) 18%, var(--color-surface) 82%);
|
||||||
|
color: color-mix(in srgb, var(--task-status-color, #5c7bd9) 72%, #0f172a 28%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(.task-inline-link:hover) {
|
||||||
|
background: #dfeaff;
|
||||||
|
border-color: #aac4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(.task-inline-link i) {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(h1),
|
||||||
|
.markdown-body :deep(h2),
|
||||||
|
.markdown-body :deep(h3) {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(p),
|
||||||
|
.markdown-body :deep(li) {
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode overrides */
|
||||||
|
:root[data-bs-theme="dark"] .note-meta {
|
||||||
|
border-bottom-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .tag-chip {
|
||||||
|
background: #1e2d5f;
|
||||||
|
color: #93b4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .pinned-chip {
|
||||||
|
color: #7dd3fc;
|
||||||
|
background: #1a3a5c;
|
||||||
|
border-color: #2d6a9f;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .featured-chip {
|
||||||
|
color: #fbbf24;
|
||||||
|
background: #3a2e0a;
|
||||||
|
border-color: #7a5a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .public-chip {
|
||||||
|
color: #67e8f9;
|
||||||
|
background: #0c2a3a;
|
||||||
|
border-color: #1d6a7a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .private-chip {
|
||||||
|
color: #c4b5fd;
|
||||||
|
background: #2d1f5e;
|
||||||
|
border-color: #5b3f9a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .protected-chip {
|
||||||
|
color: #fdba74;
|
||||||
|
background: #3a1f0a;
|
||||||
|
border-color: #7a4f1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .markdown-body :deep(.task-inline-link) {
|
||||||
|
border-color: #35508b;
|
||||||
|
background: #1b2a4a;
|
||||||
|
color: #9ec0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .markdown-body :deep(.task-inline-link:hover) {
|
||||||
|
background: #22345c;
|
||||||
|
border-color: #4566ad;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .markdown-body :deep(.task-inline-status) {
|
||||||
|
border-color: color-mix(in srgb, var(--task-status-color, #7aa2f7) 65%, #1e293b 35%);
|
||||||
|
background: color-mix(in srgb, var(--task-status-color, #7aa2f7) 26%, #111827 74%);
|
||||||
|
color: color-mix(in srgb, var(--task-status-color, #7aa2f7) 70%, #dbeafe 30%);
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
.search-results-page {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-header {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #223149;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-meta {
|
||||||
|
margin: 0.35rem 0 0;
|
||||||
|
color: #5b6f8b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-bar {
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-indicator {
|
||||||
|
color: #4f637d;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
min-height: 48vh;
|
||||||
|
border: 1px dashed #cfdae9;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: radial-gradient(circle at 20% 20%, #f2f9ff 0%, #edf2ff 70%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state-icon {
|
||||||
|
font-size: 4.2rem;
|
||||||
|
color: #60789a;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #223149;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
margin: 0.6rem 0 0;
|
||||||
|
color: #5b6f8b;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.pagination-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode overrides */
|
||||||
|
:root[data-bs-theme="dark"] .search-results-header h2 {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .search-meta {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .page-indicator {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .empty-state {
|
||||||
|
border-color: var(--color-border);
|
||||||
|
background: radial-gradient(circle at 20% 20%, #1a2035 0%, #1e2430 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .empty-state h3 {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .empty-state p {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .empty-state-icon {
|
||||||
|
color: #4a6fa5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
401
frontend/src/assets/styles/scoped/components/TaskBoard.css
Normal file
401
frontend/src/assets/styles/scoped/components/TaskBoard.css
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
.task-board {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-board-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-filters {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-lane {
|
||||||
|
border: 1px solid #d9e2ec;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: linear-gradient(180deg, #fcfdff 0%, #f5f8fc 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lane-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.55rem;
|
||||||
|
padding: 0.4rem 0.45rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid #e4e9f0;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.is-drag-over {
|
||||||
|
border-color: #7aa2f7;
|
||||||
|
background: #eef3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
color: #74839a;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-name {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-status-groups {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-group {
|
||||||
|
border: 1px solid #dbe4f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: visible;
|
||||||
|
background: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-group-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
background: #f8fbff;
|
||||||
|
border-bottom: 1px solid #edf2f8;
|
||||||
|
padding: 0.65rem 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-group-title-wrap {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-group-title {
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-group-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-group-count {
|
||||||
|
color: #5f6f87;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-empty {
|
||||||
|
padding: 0.75rem 0.85rem;
|
||||||
|
color: #7a8799;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-tree-row {
|
||||||
|
border-bottom: 1px solid #edf2f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-tree-row:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-tree-row.level-1 .task-row {
|
||||||
|
padding-left: 2.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-tree-row.level-2 .task-row {
|
||||||
|
padding-left: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-row {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 28px 1fr auto;
|
||||||
|
gap: 0.65rem;
|
||||||
|
align-items: center;
|
||||||
|
border: 0;
|
||||||
|
background: var(--color-surface);
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.7rem 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-row:hover {
|
||||||
|
background: #f4f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-group-header {
|
||||||
|
border-top-left-radius: 12px;
|
||||||
|
border-top-right-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-group > .task-tree-row:last-child .task-row,
|
||||||
|
.status-group > .task-tree-row:last-child > div > .task-tree-row:last-child .task-row,
|
||||||
|
.status-group > .task-tree-row:last-child > div > .task-tree-row:last-child > div > .task-tree-row:last-child .task-row {
|
||||||
|
border-bottom-left-radius: 12px;
|
||||||
|
border-bottom-right-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-toggle {
|
||||||
|
width: 1.25rem;
|
||||||
|
color: #5f6f87;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-main strong,
|
||||||
|
.task-main small {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-status-menu {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-trigger {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 999px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-trigger:hover {
|
||||||
|
background: #eef3f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-trigger-dot {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 2px solid var(--color-surface);
|
||||||
|
box-shadow: 0 0 0 1px rgba(67, 81, 98, 0.25);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-popup {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: calc(100% + 0.3rem);
|
||||||
|
min-width: 190px;
|
||||||
|
background: #151a22;
|
||||||
|
border: 1px solid #2a3343;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 12px 28px rgba(5, 9, 15, 0.35);
|
||||||
|
padding: 0.35rem;
|
||||||
|
z-index: 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-option {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: #e8edf5;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 14px 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.55rem;
|
||||||
|
padding: 0.45rem 0.5rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-option:hover,
|
||||||
|
.status-option.selected {
|
||||||
|
background: rgba(255, 255, 255, 0.09);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-option-dot {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 2px solid;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-option-label {
|
||||||
|
font-size: 0.86rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-option-check {
|
||||||
|
color: #e8edf5;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 1rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-color-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-zone {
|
||||||
|
border: 1px solid #f3b5b5;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background: var(--color-surface) 5f5;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-zone-title {
|
||||||
|
color: #9f1c1c;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-zone-copy {
|
||||||
|
color: #7a2727;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.task-filters {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-row {
|
||||||
|
grid-template-columns: 24px minmax(0, 1fr) auto;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-popup {
|
||||||
|
right: -0.2rem;
|
||||||
|
min-width: 170px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Dark mode ── */
|
||||||
|
:root[data-bs-theme="dark"] .status-lane {
|
||||||
|
background: linear-gradient(180deg, #1e2330 0%, #1a1d27 100%);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .status-item {
|
||||||
|
background: #252b38;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
color: #c8d3e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .status-item.is-drag-over {
|
||||||
|
border-color: #7aa2f7;
|
||||||
|
background: #1e2d4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .drag-handle {
|
||||||
|
color: #5f6f87;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .status-group {
|
||||||
|
background: #1e2230;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .status-group-header {
|
||||||
|
background: #232840;
|
||||||
|
border-bottom-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .status-group-title {
|
||||||
|
color: #c8d3e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .status-group-count {
|
||||||
|
color: #7a8fa8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .status-empty {
|
||||||
|
color: #5f6f87;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .task-tree-row {
|
||||||
|
border-bottom-color: #2e3444;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .task-row {
|
||||||
|
background: #1e2230;
|
||||||
|
color: #c8d3e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .task-row:hover {
|
||||||
|
background: #252d40;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .tree-toggle {
|
||||||
|
color: #7a8fa8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .task-main small {
|
||||||
|
color: #7a8fa8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .status-trigger:hover {
|
||||||
|
background: #2e3448;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .status-trigger-dot {
|
||||||
|
border-color: #1e2230;
|
||||||
|
box-shadow: 0 0 0 1px rgba(180, 195, 220, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .empty-state {
|
||||||
|
color: #7a8fa8;
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
.status-progress {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
color: #627086;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step.current {
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-step.done {
|
||||||
|
color: #1f7a4d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 2px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtask-row {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
border: 1px solid #dbe4f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8fbff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.35rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Dark mode ── */
|
||||||
|
:root[data-bs-theme="dark"] .progress-step {
|
||||||
|
color: #7a8fa8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .progress-step.current {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .progress-step.done {
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .subtask-row {
|
||||||
|
background: #252b38;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
color: #c8d3e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
220
frontend/src/assets/styles/scoped/components/WorkspaceList.css
Normal file
220
frontend/src/assets/styles/scoped/components/WorkspaceList.css
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
.workspace-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-workspace-state {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
min-height: 48vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px dashed #cfd6e4;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: linear-gradient(180deg, #f8f9fc 0%, #eef3fb 100%);
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-workspace-icon {
|
||||||
|
font-size: 5.25rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: #60789a;
|
||||||
|
margin-bottom: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-workspace-title {
|
||||||
|
margin: 0;
|
||||||
|
color: #23364f;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-workspace-message {
|
||||||
|
margin: 0.75rem 0 0;
|
||||||
|
max-width: 460px;
|
||||||
|
color: #4f637d;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-title {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-preview {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-icon {
|
||||||
|
color: #408aca;
|
||||||
|
font-size: 0.9em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-icon {
|
||||||
|
color: #f08c00;
|
||||||
|
font-size: 0.95em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-icon {
|
||||||
|
color: #5568a8;
|
||||||
|
font-size: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card.is-pinned {
|
||||||
|
background: #dbf5ff;
|
||||||
|
border-color: #a8d1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card.is-featured {
|
||||||
|
border-color: #ffd8a8;
|
||||||
|
background: #fff9db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card.is-task-list {
|
||||||
|
border-color: #d9e3ff;
|
||||||
|
background: #f7f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-footer {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-list--list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-list--list .content-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-list--list .content-card:hover {
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
background-color: #eef2ff;
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
border-left: 3px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-list--list .content-title {
|
||||||
|
flex: 0 0 220px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-list--list .content-preview {
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-list--list .content-card > small {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.empty-workspace-state {
|
||||||
|
min-height: 40vh;
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-workspace-icon {
|
||||||
|
font-size: 4.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-workspace-title {
|
||||||
|
font-size: 1.45rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .empty-workspace-state {
|
||||||
|
border-color: var(--color-border);
|
||||||
|
background: linear-gradient(180deg, #1e2430 0%, var(--color-surface) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .empty-workspace-title {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .empty-workspace-message {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .content-card {
|
||||||
|
border-color: var(--color-border);
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .content-card:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .workspace-list--list .content-card:hover {
|
||||||
|
background-color: #2a2f3a;
|
||||||
|
border-color: #7aa2f7;
|
||||||
|
border-left-color: #7aa2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .content-title {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .content-preview {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .content-card.is-pinned {
|
||||||
|
background: #1a3a5c;
|
||||||
|
border-color: #2d6a9f;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .content-card.is-featured {
|
||||||
|
background: #3a2e0a;
|
||||||
|
border-color: #7a5a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .content-card.is-task-list {
|
||||||
|
background: #1f2a44;
|
||||||
|
border-color: #334b7d;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .list-icon {
|
||||||
|
color: #bfceef;
|
||||||
|
}
|
||||||
234
frontend/src/assets/styles/scoped/pages/Admin.css
Normal file
234
frontend/src/assets/styles/scoped/pages/Admin.css
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
.admin-page {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-topbar {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-shell {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
gap: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar {
|
||||||
|
width: 280px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: var(--color-bg);
|
||||||
|
border-right: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar-inner {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav .nav-link {
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
color: #495057;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav .nav-link:hover {
|
||||||
|
background: #eef2f7;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav .nav-link.active {
|
||||||
|
background: #212529;
|
||||||
|
color: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-section {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.users-list .list-group-item {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-row-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-row-actions {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-actions-stack {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-meta-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 0.75rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-meta-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-meta-value {
|
||||||
|
color: #495057;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-meta-item-groups {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.user-meta-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-meta-item-groups {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.admin-shell {
|
||||||
|
display: block;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-topbar {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-content {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
z-index: 1400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: min(82vw, 320px);
|
||||||
|
z-index: 1410;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.25s ease;
|
||||||
|
border-right: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar-inner {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar.open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-row-actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-row-actions .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-actions-stack {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-meta-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.65rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode overrides */
|
||||||
|
:root[data-bs-theme="dark"] .admin-topbar {
|
||||||
|
border-bottom-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .admin-sidebar {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-right-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .admin-nav .nav-link {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .admin-nav .nav-link:hover {
|
||||||
|
background: var(--color-surface-muted);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .admin-nav .nav-link.active {
|
||||||
|
background: var(--color-text);
|
||||||
|
color: #1a1d23;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .user-meta-value {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .admin-section {
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
116
frontend/src/assets/styles/scoped/pages/Login.css
Normal file
116
frontend/src/assets/styles/scoped/pages/Login.css
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
.login-page {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 1.25rem;
|
||||||
|
background: radial-gradient(circle at 10% 10%, rgba(255, 255, 255, 0.2), transparent 45%), linear-gradient(135deg, #3554d1 0%, #4f46a5 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 460px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
background: var(--color-surface);
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 18px;
|
||||||
|
box-shadow: 0 22px 48px rgba(16, 24, 40, 0.22);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-block {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-mark {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(53, 84, 209, 0.12);
|
||||||
|
color: #2f4ac1;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2.05rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
color: #2f3237;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #2f3237;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
border-radius: 0.65rem;
|
||||||
|
min-height: 48px;
|
||||||
|
border-color: #d6dbe4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-submit {
|
||||||
|
min-height: 48px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-provider-btn {
|
||||||
|
min-height: 48px;
|
||||||
|
border-radius: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-divider::before,
|
||||||
|
.oauth-divider::after {
|
||||||
|
content: "";
|
||||||
|
flex: 1;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-divider span {
|
||||||
|
padding: 0 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-switch-link {
|
||||||
|
color: #4b5565;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.login-page {
|
||||||
|
padding: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-title {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-title {
|
||||||
|
font-size: 1.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
71
frontend/src/assets/styles/scoped/pages/PublicSpace.css
Normal file
71
frontend/src/assets/styles/scoped/pages/PublicSpace.css
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
.public-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-sidebar {
|
||||||
|
width: 280px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-item {
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-item:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-item.active {
|
||||||
|
background: #dbe4ff;
|
||||||
|
border-color: #748ffc;
|
||||||
|
color: #364fc7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-item.is-featured {
|
||||||
|
background: var(--color-surface)4e6;
|
||||||
|
border-color: #ffd8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-item.is-featured:hover {
|
||||||
|
background: #ffe8cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.public-sidebar-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 1090;
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-sidebar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1095;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.public-sidebar.open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
93
frontend/src/assets/styles/scoped/pages/Register.css
Normal file
93
frontend/src/assets/styles/scoped/pages/Register.css
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
.register-page {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 1.25rem;
|
||||||
|
background: radial-gradient(circle at 10% 10%, rgba(255, 255, 255, 0.2), transparent 45%), linear-gradient(135deg, #3554d1 0%, #4f46a5 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-card {
|
||||||
|
background: var(--color-surface);
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 18px;
|
||||||
|
box-shadow: 0 22px 48px rgba(16, 24, 40, 0.22);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-block {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-mark {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(53, 84, 209, 0.12);
|
||||||
|
color: #2f4ac1;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2.05rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
color: #2f3237;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #2f3237;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
border-radius: 0.65rem;
|
||||||
|
min-height: 48px;
|
||||||
|
border-color: #d6dbe4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-submit {
|
||||||
|
min-height: 48px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-switch-link {
|
||||||
|
color: #4b5565;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.register-page {
|
||||||
|
padding: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-card {
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-title {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-title {
|
||||||
|
font-size: 1.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ const hydrateForm = () => {
|
|||||||
form.value = {
|
form.value = {
|
||||||
name: props.group?.name || "",
|
name: props.group?.name || "",
|
||||||
description: props.group?.description || "",
|
description: props.group?.description || "",
|
||||||
permissionsText: (props.group?.permissions || []).join("\n"),
|
permissionsText: (props.group?.permissions || []).join("/n"),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,33 +93,7 @@ const handleSubmit = () => {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="../assets/styles/scoped/components/AdminGroupModal.css"></style>
|
||||||
.admin-modal {
|
|
||||||
z-index: 2000;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-top: max(0.5rem, env(safe-area-inset-top));
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-modal-backdrop {
|
|
||||||
z-index: 1990;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-modal .modal-dialog {
|
|
||||||
margin: 1rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.permissions-textarea {
|
|
||||||
font-family: "Courier New", monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
|
||||||
.admin-modal {
|
|
||||||
padding-top: max(0.75rem, env(safe-area-inset-top));
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-modal .modal-dialog {
|
|
||||||
margin: 0.75rem;
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -178,28 +178,7 @@ const handleSubmit = () => {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="../assets/styles/scoped/components/AdminProviderModal.css"></style>
|
||||||
.admin-modal {
|
|
||||||
z-index: 2000;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-top: max(0.5rem, env(safe-area-inset-top));
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-modal-backdrop {
|
|
||||||
z-index: 1990;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-modal .modal-dialog {
|
|
||||||
margin: 1rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
|
||||||
.admin-modal {
|
|
||||||
padding-top: max(0.75rem, env(safe-area-inset-top));
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-modal .modal-dialog {
|
|
||||||
margin: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -253,29 +253,7 @@ const deleteSpace = async () => {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="../assets/styles/scoped/components/AdminSpaceModal.css"></style>
|
||||||
.admin-modal {
|
|
||||||
z-index: 2000;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-top: max(0.5rem, env(safe-area-inset-top));
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-modal-backdrop {
|
|
||||||
z-index: 1990;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-modal .modal-dialog {
|
|
||||||
margin: 1rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
|
||||||
.admin-modal {
|
|
||||||
padding-top: max(0.75rem, env(safe-area-inset-top));
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-modal .modal-dialog {
|
|
||||||
margin: 0.75rem;
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -83,29 +83,7 @@ const handleSubmit = () => {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="../assets/styles/scoped/components/AdminUserModal.css"></style>
|
||||||
.admin-modal {
|
|
||||||
z-index: 2000;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-top: max(0.5rem, env(safe-area-inset-top));
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-modal-backdrop {
|
|
||||||
z-index: 1990;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-modal .modal-dialog {
|
|
||||||
margin: 1rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
|
||||||
.admin-modal {
|
|
||||||
padding-top: max(0.75rem, env(safe-area-inset-top));
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-modal .modal-dialog {
|
|
||||||
margin: 0.75rem;
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="category-tree">
|
<div class="category-tree">
|
||||||
<div v-for="category in categories" :key="category.id" class="category-item">
|
<div v-for="category in categories" :key="category.id" class="category-item">
|
||||||
<div class="category-header" @click="handleCategoryClick(category)">
|
<div class="category-header" @click="handleCategoryClick(category)">
|
||||||
<span class="expand-icon" v-if="category.subcategories?.length || category.notes?.length">
|
<span class="expand-icon" v-if="category.subcategories?.length || category.notes?.length || category.task_lists?.length">
|
||||||
<i :class="expandedCategories[category.id] ? 'mdi mdi-chevron-down' : 'mdi mdi-chevron-right'" aria-hidden="true"></i>
|
<i :class="expandedCategories[category.id] ? 'mdi mdi-chevron-down' : 'mdi mdi-chevron-right'" aria-hidden="true"></i>
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="expand-icon"> </span>
|
<span v-else class="expand-icon"> </span>
|
||||||
@@ -20,6 +20,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="expandedCategories[category.id]" class="category-content">
|
<div v-if="expandedCategories[category.id]" class="category-content">
|
||||||
|
<div v-for="taskList in category.task_lists || []" :key="taskList.id" class="task-list-item" @click.stop="onSelectTaskList(taskList)">
|
||||||
|
<i class="mdi mdi-format-list-checkbox me-1" aria-hidden="true"></i>
|
||||||
|
<span>{{ taskList.name }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="note in sortedNotes(category.notes)"
|
v-for="note in sortedNotes(category.notes)"
|
||||||
:key="note.id"
|
:key="note.id"
|
||||||
@@ -41,6 +46,7 @@
|
|||||||
:on-add-subcategory="onAddSubcategory"
|
:on-add-subcategory="onAddSubcategory"
|
||||||
:on-edit-category="onEditCategory"
|
:on-edit-category="onEditCategory"
|
||||||
:on-delete-category="onDeleteCategory"
|
:on-delete-category="onDeleteCategory"
|
||||||
|
:on-select-task-list="onSelectTaskList"
|
||||||
:can-create-categories="canCreateCategories"
|
:can-create-categories="canCreateCategories"
|
||||||
:can-edit-categories="canEditCategories"
|
:can-edit-categories="canEditCategories"
|
||||||
:can-delete-categories="canDeleteCategories"
|
:can-delete-categories="canDeleteCategories"
|
||||||
@@ -80,6 +86,10 @@ const props = defineProps({
|
|||||||
type: Function,
|
type: Function,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
onSelectTaskList: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
canCreateCategories: {
|
canCreateCategories: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@@ -141,187 +151,4 @@ const handleDeleteCategory = (category) => {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="../assets/styles/scoped/components/CategoryTree.css"></style>
|
||||||
.category-item {
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-header:hover {
|
|
||||||
background-color: #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expand-icon {
|
|
||||||
width: 20px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-name {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-actions {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-button {
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #495057;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-button:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-dropdown {
|
|
||||||
position: absolute;
|
|
||||||
top: 30px;
|
|
||||||
right: 0;
|
|
||||||
min-width: 150px;
|
|
||||||
padding: 0.35rem;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
box-shadow: 0 10px 25px rgba(15, 23, 42, 0.15);
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
text-align: left;
|
|
||||||
padding: 0.45rem 0.6rem;
|
|
||||||
border-radius: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item:hover {
|
|
||||||
background-color: #f1f3f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item.danger {
|
|
||||||
color: #c92a2a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-content {
|
|
||||||
padding-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 16px;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-item:hover {
|
|
||||||
background-color: #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-item span {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pin-icon {
|
|
||||||
color: #408aca;
|
|
||||||
font-size: 0.9em;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-icon {
|
|
||||||
color: #f08c00;
|
|
||||||
font-size: 0.9em;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-item.is-pinned {
|
|
||||||
background: #dbf5ff;
|
|
||||||
border: 1px solid #a8d1ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-item.is-pinned:hover {
|
|
||||||
background: #c5e9ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-item.is-featured {
|
|
||||||
background: #fff9db;
|
|
||||||
border: 1px solid #ffd8a8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-item.is-featured:hover {
|
|
||||||
background: #fff6c5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subcategories {
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode overrides */
|
|
||||||
:root[data-bs-theme="dark"] .category-header:hover {
|
|
||||||
background-color: #2d3748;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .menu-button {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .menu-button:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .menu-dropdown {
|
|
||||||
background: #2d3748;
|
|
||||||
border-color: #4a5568;
|
|
||||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .menu-item {
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .menu-item:hover {
|
|
||||||
background-color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .note-item:hover {
|
|
||||||
background-color: #2d3748;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .note-item.is-pinned {
|
|
||||||
background: #1a3a5c;
|
|
||||||
border-color: #2d6a9f;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .note-item.is-pinned:hover {
|
|
||||||
background: #1e4470;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .note-item.is-featured {
|
|
||||||
background: #3a2e0a;
|
|
||||||
border-color: #7a5a0a;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .note-item.is-featured:hover {
|
|
||||||
background: #453710;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -90,4 +90,5 @@ const handleSubmit = () => {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|
||||||
|
|||||||
@@ -141,16 +141,7 @@ const handleCreate = () => {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="../assets/styles/scoped/components/CreateNoteModal.css"></style>
|
||||||
.note-flags {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flag-check {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.45rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -49,4 +49,5 @@ const handleCreate = () => {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|
||||||
|
|||||||
84
frontend/src/components/CreateTaskListModal.vue
Normal file
84
frontend/src/components/CreateTaskListModal.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<teleport to="body">
|
||||||
|
<div class="modal fade show d-block" tabindex="-1" role="dialog" aria-modal="true" @click.self="closeModal">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Create New Task List</h5>
|
||||||
|
<button type="button" class="btn-close" aria-label="Close" @click="closeModal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form @submit.prevent="handleCreate">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="taskListName" class="form-label">Task List Name</label>
|
||||||
|
<input id="taskListName" v-model="form.name" type="text" class="form-control" maxlength="120" required />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="taskListCategory" class="form-label">Category</label>
|
||||||
|
<select id="taskListCategory" v-model="form.category_id" class="form-select">
|
||||||
|
<option :value="null">No category</option>
|
||||||
|
<option v-for="category in categoryOptions" :key="category.id" :value="category.id">
|
||||||
|
{{ category.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Create</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-backdrop fade show"></div>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
categoryOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
defaultCategoryId: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["close", "create"]);
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
name: "",
|
||||||
|
category_id: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.defaultCategoryId,
|
||||||
|
(defaultCategoryId) => {
|
||||||
|
form.value.category_id = defaultCategoryId || null;
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
emit("close");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = () => {
|
||||||
|
const name = form.value.name.trim();
|
||||||
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit("create", {
|
||||||
|
name,
|
||||||
|
category_id: form.value.category_id || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
form.value = {
|
||||||
|
name: "",
|
||||||
|
category_id: props.defaultCategoryId || null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -217,7 +217,7 @@ const createFolder = async () => {
|
|||||||
|
|
||||||
const deleteItem = async (obj) => {
|
const deleteItem = async (obj) => {
|
||||||
const label = displayName(obj);
|
const label = displayName(obj);
|
||||||
if (!confirm(`Delete "${label}"?${obj.is_folder ? "\n\nThis will delete all files inside the folder." : ""}`)) return;
|
if (!confirm(`Delete "${label}"?${obj.is_folder ? "./nThis will delete all files inside the folder." : ""}`)) return;
|
||||||
error.value = "";
|
error.value = "";
|
||||||
try {
|
try {
|
||||||
if (obj.is_folder) {
|
if (obj.is_folder) {
|
||||||
@@ -284,67 +284,4 @@ watch(showNewFolderInput, async (v) => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="../assets/styles/scoped/components/FileExplorer.css"></style>
|
||||||
.file-explorer {
|
|
||||||
background: #fff;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-explorer-header {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
min-height: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-list {
|
|
||||||
max-height: 480px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-item {
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.1s;
|
|
||||||
color: #333;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-item:hover {
|
|
||||||
background-color: #f0f4ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-active {
|
|
||||||
outline: 2px dashed #0d6efd;
|
|
||||||
outline-offset: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-delete {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-item:hover .btn-delete {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode overrides */
|
|
||||||
:root[data-bs-theme="dark"] .file-explorer {
|
|
||||||
background: #21252e;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .file-explorer-header {
|
|
||||||
background: #21252e;
|
|
||||||
border-color: #3a3f4b;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .file-item {
|
|
||||||
border-bottom-color: #3a3f4b;
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .file-item:hover {
|
|
||||||
background-color: #2d3748;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -187,95 +187,7 @@ const createProvider = async () => {
|
|||||||
onMounted(loadProviders);
|
onMounted(loadProviders);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="../assets/styles/scoped/components/ManageAuthProvidersModal.css"></style>
|
||||||
.modal-backdrop-custom {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(15, 23, 42, 0.45);
|
|
||||||
backdrop-filter: blur(2px);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 2000;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-panel {
|
|
||||||
width: min(920px, 100%);
|
|
||||||
max-height: min(92vh, 980px);
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #dbe3ee;
|
|
||||||
border-radius: 14px;
|
|
||||||
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.2);
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-modal-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 1rem 1.25rem;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-modal-title {
|
|
||||||
margin: 0;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-modal-close {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-modal-body {
|
|
||||||
padding: 1rem 1.25rem 1.25rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-section {
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: #f8fafc;
|
|
||||||
padding: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-list {
|
|
||||||
max-height: 220px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.modal-backdrop-custom {
|
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-modal-header,
|
|
||||||
.provider-modal-body {
|
|
||||||
padding-left: 0.85rem;
|
|
||||||
padding-right: 0.85rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode overrides */
|
|
||||||
:root[data-bs-theme="dark"] .modal-panel {
|
|
||||||
background: #21252e;
|
|
||||||
border-color: #3a3f4b;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .provider-modal-header {
|
|
||||||
background: linear-gradient(180deg, #2a2f3a 0%, #21252e 100%);
|
|
||||||
border-bottom-color: #3a3f4b;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .provider-section {
|
|
||||||
background: #2a2f3a;
|
|
||||||
border-color: #3a3f4b;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -571,303 +571,4 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="../assets/styles/scoped/components/NoteEditor.css"></style>
|
||||||
.editor-toolbar {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
border-bottom: 1px solid #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-status {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #6c757d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-status.dirty {
|
|
||||||
color: #b26a00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-status.saving {
|
|
||||||
color: #0d6efd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-status.saved {
|
|
||||||
color: #2b8a3e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-textarea {
|
|
||||||
font-family: "Courier New", monospace;
|
|
||||||
min-height: 600px;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-flags {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flag-check {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.45rem;
|
|
||||||
padding: 0.45rem 0.75rem;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #f8f9fa;
|
|
||||||
margin: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flag-check-input {
|
|
||||||
margin: 0;
|
|
||||||
width: 1.1rem;
|
|
||||||
height: 1.1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flag-check-label {
|
|
||||||
line-height: 1;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-pane {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danger-zone {
|
|
||||||
padding: 1rem;
|
|
||||||
border: 1px solid #f3b5b5;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
background: #fff5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danger-zone-title {
|
|
||||||
color: #9f1c1c;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danger-zone-copy {
|
|
||||||
color: #7a2727;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-mention-panel {
|
|
||||||
margin-top: 0.45rem;
|
|
||||||
border: 1px solid #dbe4f0;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: #fbfdff;
|
|
||||||
padding: 0.5rem;
|
|
||||||
max-height: 220px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-mention-option {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
padding: 0.35rem 0.45rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-mention-option:hover {
|
|
||||||
background: #eef3ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-picker {
|
|
||||||
background: #fff;
|
|
||||||
min-height: 300px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-picker-header {
|
|
||||||
background: #f8f9fa;
|
|
||||||
min-height: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-picker-search {
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-picker-list {
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 520px;
|
|
||||||
padding: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-picker-item {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.35rem 0.45rem;
|
|
||||||
text-align: left;
|
|
||||||
gap: 0.4rem;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-picker-item:hover {
|
|
||||||
background: #eef3ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-picker-title {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-picker-empty {
|
|
||||||
padding: 0.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-picker-item small {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-picker .btn-link {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-picker-item {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body :deep(.task-inline-link) {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
padding: 0.18rem 0.5rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
border: 1px solid #c7d8ff;
|
|
||||||
background: #eef4ff;
|
|
||||||
color: #2c4ea3;
|
|
||||||
font-weight: 600;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body :deep(.task-inline-title) {
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body :deep(.task-inline-status) {
|
|
||||||
line-height: 1;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 700;
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 0.16rem 0.42rem;
|
|
||||||
border: 1px solid color-mix(in srgb, var(--task-status-color, #5c7bd9) 60%, #ffffff 40%);
|
|
||||||
background: color-mix(in srgb, var(--task-status-color, #5c7bd9) 18%, #ffffff 82%);
|
|
||||||
color: color-mix(in srgb, var(--task-status-color, #5c7bd9) 72%, #0f172a 28%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body :deep(.task-inline-link:hover) {
|
|
||||||
background: #dfeaff;
|
|
||||||
border-color: #aac4ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body :deep(.task-inline-link i) {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode overrides */
|
|
||||||
:root[data-bs-theme="dark"] .editor-toolbar {
|
|
||||||
border-bottom-color: #3a3f4b;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .flag-check {
|
|
||||||
background: #2d3748;
|
|
||||||
border-color: #4a5568;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .preview-pane {
|
|
||||||
background-color: #21252e;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .danger-zone {
|
|
||||||
background: #2d1a1a;
|
|
||||||
border-color: #7a3030;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .danger-zone-title {
|
|
||||||
color: #fc8181;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .danger-zone-copy {
|
|
||||||
color: #fca5a5;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .task-mention-panel {
|
|
||||||
border-color: #3a4558;
|
|
||||||
background: #1f2733;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .task-mention-option:hover {
|
|
||||||
background: #2b3646;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .task-picker {
|
|
||||||
border-color: #3a3f4b !important;
|
|
||||||
background: #21252e;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .task-picker-header {
|
|
||||||
background: #21252e;
|
|
||||||
border-bottom-color: #3a3f4b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .task-picker-search {
|
|
||||||
background: #21252e;
|
|
||||||
border-bottom-color: #3a3f4b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .task-picker-search .form-control {
|
|
||||||
background: #1f2430;
|
|
||||||
border-color: #3a3f4b;
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .task-picker-item {
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .task-picker-item:hover {
|
|
||||||
background: #2d3748;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .task-picker-item small {
|
|
||||||
color: #a8b4c7;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .markdown-body :deep(.task-inline-link) {
|
|
||||||
border-color: #35508b;
|
|
||||||
background: #1b2a4a;
|
|
||||||
color: #9ec0ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .markdown-body :deep(.task-inline-link:hover) {
|
|
||||||
background: #22345c;
|
|
||||||
border-color: #4566ad;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .markdown-body :deep(.task-inline-status) {
|
|
||||||
border-color: color-mix(in srgb, var(--task-status-color, #7aa2f7) 65%, #1e293b 35%);
|
|
||||||
background: color-mix(in srgb, var(--task-status-color, #7aa2f7) 26%, #111827 74%);
|
|
||||||
color: color-mix(in srgb, var(--task-status-color, #7aa2f7) 70%, #dbeafe 30%);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,272 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="note-list" :class="{ 'note-list--list': viewMode === 'list' }">
|
|
||||||
<div v-if="notes.length === 0" class="empty-notes-state" role="status" aria-live="polite">
|
|
||||||
<i class="mdi mdi-file-document-outline empty-notes-icon" aria-hidden="true"></i>
|
|
||||||
<h3 class="empty-notes-title">No Notes Yet</h3>
|
|
||||||
<p class="empty-notes-message">This space is empty for now. Create your first note to get started.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-for="note in notes" :key="note.id" class="note-card" :class="{ 'is-pinned': note.is_pinned, 'is-featured': note.is_favorite || note.is_featured }" @click="selectNote(note)">
|
|
||||||
<h5 class="note-title">
|
|
||||||
<i v-if="note.is_pinned" class="mdi mdi-pin pin-icon" aria-hidden="true"></i>
|
|
||||||
<i v-else-if="note.is_favorite || note.is_featured" class="mdi mdi-star featured-icon" aria-hidden="true"></i>
|
|
||||||
{{ note.title }}
|
|
||||||
</h5>
|
|
||||||
<p class="note-preview">{{ getDescription(note) }}</p>
|
|
||||||
<small class="text-muted">Updated: {{ formatDate(note.updated_at) }}</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="canLoadMore" class="list-footer">
|
|
||||||
<button class="btn btn-outline-secondary" :disabled="isLoadingMore" @click="emit('loadMore')">
|
|
||||||
{{ isLoadingMore ? "Loading..." : "Load more" }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
defineProps({
|
|
||||||
notes: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
canLoadMore: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
isLoadingMore: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
viewMode: {
|
|
||||||
type: String,
|
|
||||||
default: "grid",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(["selectNote", "loadMore"]);
|
|
||||||
|
|
||||||
const selectNote = (note) => {
|
|
||||||
emit("selectNote", note);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
|
||||||
return new Date(dateString).toLocaleDateString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDescription = (note) => {
|
|
||||||
const description = (note?.description || "").trim();
|
|
||||||
if (!description) {
|
|
||||||
return "No description";
|
|
||||||
}
|
|
||||||
return description;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.note-list {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-notes-state {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
min-height: 48vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
border: 1px dashed #cfd6e4;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: linear-gradient(180deg, #f8f9fc 0%, #eef3fb 100%);
|
|
||||||
padding: 2rem 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-notes-icon {
|
|
||||||
font-size: 5.25rem;
|
|
||||||
line-height: 1;
|
|
||||||
color: #60789a;
|
|
||||||
margin-bottom: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-notes-title {
|
|
||||||
margin: 0;
|
|
||||||
color: #23364f;
|
|
||||||
font-size: 1.8rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-notes-message {
|
|
||||||
margin: 0.75rem 0 0;
|
|
||||||
max-width: 460px;
|
|
||||||
color: #4f637d;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-card {
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-card:hover {
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-title {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
color: #333;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pin-icon {
|
|
||||||
color: #408aca;
|
|
||||||
font-size: 0.9em;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-icon {
|
|
||||||
color: #f08c00;
|
|
||||||
font-size: 0.95em;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-card.is-pinned {
|
|
||||||
background: #dbf5ff;
|
|
||||||
border-color: #a8d1ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-card.is-featured {
|
|
||||||
border-color: #ffd8a8;
|
|
||||||
background: #fff9db;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-preview {
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-footer {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* List view overrides */
|
|
||||||
.note-list--list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-list--list .note-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 0.6rem 1rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-list--list .note-card:hover {
|
|
||||||
transform: none;
|
|
||||||
box-shadow: none;
|
|
||||||
background-color: #eef2ff;
|
|
||||||
border-color: #667eea;
|
|
||||||
border-left: 3px solid #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-list--list .note-title {
|
|
||||||
flex: 0 0 220px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-list--list .note-preview {
|
|
||||||
flex: 1;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-list--list .note-card > small {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-list--list .list-footer {
|
|
||||||
grid-column: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.empty-notes-state {
|
|
||||||
min-height: 40vh;
|
|
||||||
padding: 1.5rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-notes-icon {
|
|
||||||
font-size: 4.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-notes-title {
|
|
||||||
font-size: 1.45rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode overrides */
|
|
||||||
:root[data-bs-theme="dark"] .empty-notes-state {
|
|
||||||
border-color: #3a3f4b;
|
|
||||||
background: linear-gradient(180deg, #1e2430 0%, #21252e 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .empty-notes-title {
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .empty-notes-message {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .note-card {
|
|
||||||
border-color: #3a3f4b;
|
|
||||||
background-color: #21252e;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .note-card:hover {
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .note-list--list .note-card:hover {
|
|
||||||
background-color: #2a2f3a;
|
|
||||||
border-color: #7aa2f7;
|
|
||||||
border-left-color: #7aa2f7;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .note-title {
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .note-preview {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .note-card.is-pinned {
|
|
||||||
background: #1a3a5c;
|
|
||||||
border-color: #2d6a9f;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .note-card.is-featured {
|
|
||||||
background: #3a2e0a;
|
|
||||||
border-color: #7a5a0a;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -152,196 +152,4 @@ const onMarkdownClick = (event) => {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="../assets/styles/scoped/components/NoteViewer.css"></style>
|
||||||
.note-viewer {
|
|
||||||
max-width: 900px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-meta {
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
border-bottom: 1px solid #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-grid {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-chip {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.2rem 0.55rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #eef2ff;
|
|
||||||
color: #364fc7;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.state-chip {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.25rem 0.6rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pinned-chip {
|
|
||||||
color: #005f8f;
|
|
||||||
background: #dbf5ff;
|
|
||||||
border: 1px solid #a8d1ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-chip {
|
|
||||||
color: #8d7619;
|
|
||||||
|
|
||||||
border: 1px solid #ffd8a8;
|
|
||||||
background: #fff9db;
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-chip {
|
|
||||||
color: #0c5460;
|
|
||||||
border: 1px solid #a5d8ff;
|
|
||||||
background: #e7f5ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.private-chip {
|
|
||||||
color: #5f3dc4;
|
|
||||||
border: 1px solid #d0bfff;
|
|
||||||
background: #f3f0ff;
|
|
||||||
}
|
|
||||||
.protected-chip {
|
|
||||||
color: #7f5539;
|
|
||||||
border: 1px solid #e0c3a6;
|
|
||||||
background: #fff4e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body :deep(.task-inline-link) {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
padding: 0.18rem 0.5rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
border: 1px solid #c7d8ff;
|
|
||||||
background: #eef4ff;
|
|
||||||
color: #2c4ea3;
|
|
||||||
font-weight: 600;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body :deep(.task-inline-title) {
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body :deep(.task-inline-status) {
|
|
||||||
line-height: 1;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 700;
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 0.16rem 0.42rem;
|
|
||||||
border: 1px solid color-mix(in srgb, var(--task-status-color, #5c7bd9) 60%, #ffffff 40%);
|
|
||||||
background: color-mix(in srgb, var(--task-status-color, #5c7bd9) 18%, #ffffff 82%);
|
|
||||||
color: color-mix(in srgb, var(--task-status-color, #5c7bd9) 72%, #0f172a 28%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body :deep(.task-inline-link:hover) {
|
|
||||||
background: #dfeaff;
|
|
||||||
border-color: #aac4ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body :deep(.task-inline-link i) {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body :deep(h1),
|
|
||||||
.markdown-body :deep(h2),
|
|
||||||
.markdown-body :deep(h3) {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body :deep(p),
|
|
||||||
.markdown-body :deep(li) {
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body :deep(pre) {
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body :deep(code) {
|
|
||||||
font-family: "Courier New", monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body :deep(blockquote) {
|
|
||||||
margin: 1rem 0;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border-left: 4px solid #748ffc;
|
|
||||||
background: #f8f9ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode overrides */
|
|
||||||
:root[data-bs-theme="dark"] .note-meta {
|
|
||||||
border-bottom-color: #3a3f4b;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .tag-chip {
|
|
||||||
background: #1e2d5f;
|
|
||||||
color: #93b4ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .pinned-chip {
|
|
||||||
color: #7dd3fc;
|
|
||||||
background: #1a3a5c;
|
|
||||||
border-color: #2d6a9f;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .featured-chip {
|
|
||||||
color: #fbbf24;
|
|
||||||
background: #3a2e0a;
|
|
||||||
border-color: #7a5a0a;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .public-chip {
|
|
||||||
color: #67e8f9;
|
|
||||||
background: #0c2a3a;
|
|
||||||
border-color: #1d6a7a;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .private-chip {
|
|
||||||
color: #c4b5fd;
|
|
||||||
background: #2d1f5e;
|
|
||||||
border-color: #5b3f9a;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .protected-chip {
|
|
||||||
color: #fdba74;
|
|
||||||
background: #3a1f0a;
|
|
||||||
border-color: #7a4f1a;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .markdown-body :deep(blockquote) {
|
|
||||||
background: #1e2430;
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .markdown-body :deep(.task-inline-link) {
|
|
||||||
border-color: #35508b;
|
|
||||||
background: #1b2a4a;
|
|
||||||
color: #9ec0ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .markdown-body :deep(.task-inline-link:hover) {
|
|
||||||
background: #22345c;
|
|
||||||
border-color: #4566ad;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .markdown-body :deep(.task-inline-status) {
|
|
||||||
border-color: color-mix(in srgb, var(--task-status-color, #7aa2f7) 65%, #1e293b 35%);
|
|
||||||
background: color-mix(in srgb, var(--task-status-color, #7aa2f7) 26%, #111827 74%);
|
|
||||||
color: color-mix(in srgb, var(--task-status-color, #7aa2f7) 70%, #dbeafe 30%);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -3,23 +3,23 @@
|
|||||||
<header class="search-results-header">
|
<header class="search-results-header">
|
||||||
<h2>Search Results</h2>
|
<h2>Search Results</h2>
|
||||||
<p v-if="query" class="search-meta">{{ totalResults }} matches for "{{ query }}"</p>
|
<p v-if="query" class="search-meta">{{ totalResults }} matches for "{{ query }}"</p>
|
||||||
<p v-else class="search-meta">Type in the top bar and press Enter to search notes.</p>
|
<p v-else class="search-meta">Type in the top bar and press Enter to search notes and task lists.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div v-if="!query" class="empty-state">
|
<div v-if="!query" class="empty-state">
|
||||||
<i class="mdi mdi-magnify empty-state-icon" aria-hidden="true"></i>
|
<i class="mdi mdi-magnify empty-state-icon" aria-hidden="true"></i>
|
||||||
<h3>Start your search</h3>
|
<h3>Start your search</h3>
|
||||||
<p>Use a title, content keyword, or tag to find matching notes in the selected space.</p>
|
<p>Use a title, content keyword, or tag to find matching notes and task lists in the selected space.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="totalResults === 0" class="empty-state">
|
<div v-else-if="totalResults === 0" class="empty-state">
|
||||||
<i class="mdi mdi-file-search-outline empty-state-icon" aria-hidden="true"></i>
|
<i class="mdi mdi-file-search-outline empty-state-icon" aria-hidden="true"></i>
|
||||||
<h3>No matching notes</h3>
|
<h3>No matching results</h3>
|
||||||
<p>Try different keywords or a shorter phrase.</p>
|
<p>Try different keywords or a shorter phrase.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<NoteList :notes="paginatedNotes" :view-mode="viewMode" @select-note="emit('select-note', $event)" />
|
<WorkspaceList :items="paginatedItems" :view-mode="viewMode" @select-note="emit('select-note', $event)" @select-task-list="emit('select-task-list', $event)" />
|
||||||
|
|
||||||
<nav v-if="totalPages > 1" class="pagination-bar" aria-label="Search result pages">
|
<nav v-if="totalPages > 1" class="pagination-bar" aria-label="Search result pages">
|
||||||
<button class="btn btn-outline-secondary" :disabled="currentPage <= 1" @click="goToPage(currentPage - 1)">Previous</button>
|
<button class="btn btn-outline-secondary" :disabled="currentPage <= 1" @click="goToPage(currentPage - 1)">Previous</button>
|
||||||
@@ -32,14 +32,14 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import NoteList from "./NoteList.vue";
|
import WorkspaceList from "./WorkspaceList.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
query: {
|
query: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
notes: {
|
items: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
@@ -57,9 +57,9 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["select-note", "page-change"]);
|
const emit = defineEmits(["select-note", "select-task-list", "page-change"]);
|
||||||
|
|
||||||
const totalResults = computed(() => props.notes.length);
|
const totalResults = computed(() => props.items.length);
|
||||||
const totalPages = computed(() => Math.max(1, Math.ceil(totalResults.value / props.pageSize)));
|
const totalPages = computed(() => Math.max(1, Math.ceil(totalResults.value / props.pageSize)));
|
||||||
|
|
||||||
const normalizedPage = computed(() => {
|
const normalizedPage = computed(() => {
|
||||||
@@ -69,9 +69,9 @@ const normalizedPage = computed(() => {
|
|||||||
return Math.min(props.currentPage, totalPages.value);
|
return Math.min(props.currentPage, totalPages.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const paginatedNotes = computed(() => {
|
const paginatedItems = computed(() => {
|
||||||
const start = (normalizedPage.value - 1) * props.pageSize;
|
const start = (normalizedPage.value - 1) * props.pageSize;
|
||||||
return props.notes.slice(start, start + props.pageSize);
|
return props.items.slice(start, start + props.pageSize);
|
||||||
});
|
});
|
||||||
|
|
||||||
const goToPage = (page) => {
|
const goToPage = (page) => {
|
||||||
@@ -82,103 +82,4 @@ const goToPage = (page) => {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="../assets/styles/scoped/components/SearchResultsPage.css"></style>
|
||||||
.search-results-page {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-results-header {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-results-header h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
color: #223149;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-meta {
|
|
||||||
margin: 0.35rem 0 0;
|
|
||||||
color: #5b6f8b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-bar {
|
|
||||||
margin-top: 1.25rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-indicator {
|
|
||||||
color: #4f637d;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
min-height: 48vh;
|
|
||||||
border: 1px dashed #cfdae9;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: radial-gradient(circle at 20% 20%, #f2f9ff 0%, #edf2ff 70%);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state-icon {
|
|
||||||
font-size: 4.2rem;
|
|
||||||
color: #60789a;
|
|
||||||
margin-bottom: 0.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: #223149;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state p {
|
|
||||||
margin: 0.6rem 0 0;
|
|
||||||
color: #5b6f8b;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.pagination-bar {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode overrides */
|
|
||||||
:root[data-bs-theme="dark"] .search-results-header h2 {
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .search-meta {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .page-indicator {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .empty-state {
|
|
||||||
border-color: #3a3f4b;
|
|
||||||
background: radial-gradient(circle at 20% 20%, #1a2035 0%, #1e2430 70%);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .empty-state h3 {
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .empty-state p {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .empty-state-icon {
|
|
||||||
color: #4a6fa5;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -5,19 +5,9 @@
|
|||||||
<h4 class="mb-0">Tasks</h4>
|
<h4 class="mb-0">Tasks</h4>
|
||||||
<p class="text-muted small mb-0">Track work with ordered statuses.</p>
|
<p class="text-muted small mb-0">Track work with ordered statuses.</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" @click="emit('create-task')">
|
|
||||||
<i class="mdi mdi-checkbox-marked-circle-plus-outline me-1" aria-hidden="true"></i>
|
|
||||||
New Task
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="task-filters">
|
<div class="task-filters">
|
||||||
<select v-model="filterCategory" class="form-select" @change="emitFilters">
|
|
||||||
<option value="">All categories</option>
|
|
||||||
<option v-for="category in categoryOptions" :key="category.id" :value="category.id">
|
|
||||||
{{ category.label }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<select v-model="filterStatus" class="form-select" @change="emitFilters">
|
<select v-model="filterStatus" class="form-select" @change="emitFilters">
|
||||||
<option value="">All statuses</option>
|
<option value="">All statuses</option>
|
||||||
<option v-for="status in statuses" :key="status.id" :value="status.id">
|
<option v-for="status in statuses" :key="status.id" :value="status.id">
|
||||||
@@ -195,6 +185,12 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section v-if="selectedTaskList && canDeleteTaskList" class="danger-zone" aria-labelledby="task-list-danger-zone-title">
|
||||||
|
<h6 id="task-list-danger-zone-title" class="danger-zone-title">Danger Zone</h6>
|
||||||
|
<p class="danger-zone-copy mb-2">Delete this task list and all associated tasks permanently.</p>
|
||||||
|
<button type="button" class="btn btn-outline-danger" @click="emitDeleteTaskList">Delete Task List</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
<teleport to="body">
|
<teleport to="body">
|
||||||
<div v-if="showStatusModal" class="modal fade show d-block" tabindex="-1" role="dialog" aria-modal="true" @click.self="closeStatusModal">
|
<div v-if="showStatusModal" class="modal fade show d-block" tabindex="-1" role="dialog" aria-modal="true" @click.self="closeStatusModal">
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
@@ -245,15 +241,18 @@ const props = defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
categoryOptions: {
|
selectedTaskList: {
|
||||||
type: Array,
|
type: Object,
|
||||||
default: () => [],
|
default: null,
|
||||||
|
},
|
||||||
|
canDeleteTaskList: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["create-task", "select-task", "filter-change", "reorder-status", "create-status", "rename-status", "delete-status", "update-task-status"]);
|
const emit = defineEmits(["create-task", "select-task", "filter-change", "reorder-status", "create-status", "rename-status", "delete-status", "update-task-status", "delete-task-list"]);
|
||||||
|
|
||||||
const filterCategory = ref("");
|
|
||||||
const filterStatus = ref("");
|
const filterStatus = ref("");
|
||||||
const filterParent = ref("");
|
const filterParent = ref("");
|
||||||
const showStatusModal = ref(false);
|
const showStatusModal = ref(false);
|
||||||
@@ -307,7 +306,6 @@ const statusSections = computed(() =>
|
|||||||
|
|
||||||
const emitFilters = () => {
|
const emitFilters = () => {
|
||||||
emit("filter-change", {
|
emit("filter-change", {
|
||||||
categoryId: filterCategory.value || null,
|
|
||||||
statusId: filterStatus.value || null,
|
statusId: filterStatus.value || null,
|
||||||
parentTaskId: filterParent.value || null,
|
parentTaskId: filterParent.value || null,
|
||||||
});
|
});
|
||||||
@@ -486,407 +484,13 @@ const deleteStatusFromModal = () => {
|
|||||||
});
|
});
|
||||||
closeStatusModal();
|
closeStatusModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const emitDeleteTaskList = () => {
|
||||||
|
if (!props.selectedTaskList?.id || !props.canDeleteTaskList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit("delete-task-list", props.selectedTaskList);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="../assets/styles/scoped/components/TaskBoard.css"></style>
|
||||||
.task-board {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-board-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-filters {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-lane {
|
|
||||||
border: 1px solid #d9e2ec;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 0.75rem;
|
|
||||||
background: linear-gradient(180deg, #fcfdff 0%, #f5f8fc 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lane-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.45rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.55rem;
|
|
||||||
padding: 0.4rem 0.45rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #e4e9f0;
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-item.is-drag-over {
|
|
||||||
border-color: #7aa2f7;
|
|
||||||
background: #eef3ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-handle {
|
|
||||||
color: #74839a;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-name {
|
|
||||||
flex: 1;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-actions {
|
|
||||||
display: inline-flex;
|
|
||||||
gap: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-status-groups {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-group {
|
|
||||||
border: 1px solid #dbe4f0;
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: visible;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-group-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-left: 6px solid transparent;
|
|
||||||
background: #f8fbff;
|
|
||||||
border-bottom: 1px solid #edf2f8;
|
|
||||||
padding: 0.65rem 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-group-title-wrap {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-group-title {
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-group-dot {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-group-count {
|
|
||||||
color: #5f6f87;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-empty {
|
|
||||||
padding: 0.75rem 0.85rem;
|
|
||||||
color: #7a8799;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-tree-row {
|
|
||||||
border-bottom: 1px solid #edf2f8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-tree-row:last-child {
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-tree-row.level-1 .task-row {
|
|
||||||
padding-left: 2.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-tree-row.level-2 .task-row {
|
|
||||||
padding-left: 3.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-row {
|
|
||||||
width: 100%;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 28px 1fr auto;
|
|
||||||
gap: 0.65rem;
|
|
||||||
align-items: center;
|
|
||||||
border: 0;
|
|
||||||
background: #fff;
|
|
||||||
text-align: left;
|
|
||||||
padding: 0.7rem 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-row:hover {
|
|
||||||
background: #f4f8ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-group-header {
|
|
||||||
border-top-left-radius: 12px;
|
|
||||||
border-top-right-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-group > .task-tree-row:last-child .task-row,
|
|
||||||
.status-group > .task-tree-row:last-child > div > .task-tree-row:last-child .task-row,
|
|
||||||
.status-group > .task-tree-row:last-child > div > .task-tree-row:last-child > div > .task-tree-row:last-child .task-row {
|
|
||||||
border-bottom-left-radius: 12px;
|
|
||||||
border-bottom-right-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-toggle {
|
|
||||||
width: 1.25rem;
|
|
||||||
color: #5f6f87;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-main {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-main strong,
|
|
||||||
.task-main small {
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-status-menu {
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-trigger {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 999px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-trigger:hover {
|
|
||||||
background: #eef3f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-trigger-dot {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
border: 2px solid #fff;
|
|
||||||
box-shadow: 0 0 0 1px rgba(67, 81, 98, 0.25);
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-popup {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: calc(100% + 0.3rem);
|
|
||||||
min-width: 190px;
|
|
||||||
background: #151a22;
|
|
||||||
border: 1px solid #2a3343;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 12px 28px rgba(5, 9, 15, 0.35);
|
|
||||||
padding: 0.35rem;
|
|
||||||
z-index: 40;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-option {
|
|
||||||
width: 100%;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: transparent;
|
|
||||||
color: #e8edf5;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 14px 1fr auto;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.55rem;
|
|
||||||
padding: 0.45rem 0.5rem;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-option:hover,
|
|
||||||
.status-option.selected {
|
|
||||||
background: rgba(255, 255, 255, 0.09);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-option-dot {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
border-radius: 999px;
|
|
||||||
border: 2px solid;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-option-label {
|
|
||||||
font-size: 0.86rem;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-option-check {
|
|
||||||
color: #e8edf5;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
padding: 1rem;
|
|
||||||
color: #6c757d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-color-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danger-zone {
|
|
||||||
border: 1px solid #f3b5b5;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
background: #fff5f5;
|
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danger-zone-title {
|
|
||||||
color: #9f1c1c;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danger-zone-copy {
|
|
||||||
color: #7a2727;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.task-filters {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-row {
|
|
||||||
grid-template-columns: 24px 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-popup {
|
|
||||||
right: -0.2rem;
|
|
||||||
min-width: 170px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Dark mode ── */
|
|
||||||
:root[data-bs-theme="dark"] .status-lane {
|
|
||||||
background: linear-gradient(180deg, #1e2330 0%, #1a1d27 100%);
|
|
||||||
border-color: #3a3f4b;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .status-item {
|
|
||||||
background: #252b38;
|
|
||||||
border-color: #3a3f4b;
|
|
||||||
color: #c8d3e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .status-item.is-drag-over {
|
|
||||||
border-color: #7aa2f7;
|
|
||||||
background: #1e2d4a;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .drag-handle {
|
|
||||||
color: #5f6f87;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .status-group {
|
|
||||||
background: #1e2230;
|
|
||||||
border-color: #3a3f4b;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .status-group-header {
|
|
||||||
background: #232840;
|
|
||||||
border-bottom-color: #3a3f4b;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .status-group-title {
|
|
||||||
color: #c8d3e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .status-group-count {
|
|
||||||
color: #7a8fa8;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .status-empty {
|
|
||||||
color: #5f6f87;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .task-tree-row {
|
|
||||||
border-bottom-color: #2e3444;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .task-row {
|
|
||||||
background: #1e2230;
|
|
||||||
color: #c8d3e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .task-row:hover {
|
|
||||||
background: #252d40;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .tree-toggle {
|
|
||||||
color: #7a8fa8;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .task-main small {
|
|
||||||
color: #7a8fa8;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .status-trigger:hover {
|
|
||||||
background: #2e3448;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .status-trigger-dot {
|
|
||||||
border-color: #1e2230;
|
|
||||||
box-shadow: 0 0 0 1px rgba(180, 195, 220, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .empty-state {
|
|
||||||
color: #7a8fa8;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -16,12 +16,6 @@
|
|||||||
<label class="form-label mt-3">Description</label>
|
<label class="form-label mt-3">Description</label>
|
||||||
<textarea v-model="localTask.description" class="form-control" rows="5" maxlength="2000"></textarea>
|
<textarea v-model="localTask.description" class="form-control" rows="5" maxlength="2000"></textarea>
|
||||||
|
|
||||||
<label class="form-label mt-3">Category</label>
|
|
||||||
<select v-model="localTask.category_id" class="form-select">
|
|
||||||
<option value="">Uncategorized</option>
|
|
||||||
<option v-for="category in categoryOptions" :key="category.id" :value="category.id">{{ category.label }}</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label class="form-label mt-3">Parent Task</label>
|
<label class="form-label mt-3">Parent Task</label>
|
||||||
<select v-model="localTask.parent_task_id" class="form-select">
|
<select v-model="localTask.parent_task_id" class="form-select">
|
||||||
<option value="">No parent (top level)</option>
|
<option value="">No parent (top level)</option>
|
||||||
@@ -83,10 +77,6 @@ const props = defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
categoryOptions: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
parentTaskOptions: {
|
parentTaskOptions: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
@@ -107,12 +97,12 @@ watch(
|
|||||||
localTask.value = {
|
localTask.value = {
|
||||||
title: "",
|
title: "",
|
||||||
description: "",
|
description: "",
|
||||||
category_id: "",
|
task_list_id: "",
|
||||||
status_id: props.statuses[0]?.id || "",
|
status_id: props.statuses[0]?.id || "",
|
||||||
parent_task_id: "",
|
parent_task_id: "",
|
||||||
note_links: [],
|
note_links: [],
|
||||||
...value,
|
...value,
|
||||||
category_id: value?.category_id || "",
|
task_list_id: value?.task_list_id || "",
|
||||||
parent_task_id: value?.parent_task_id || "",
|
parent_task_id: value?.parent_task_id || "",
|
||||||
note_links: value?.note_links || [],
|
note_links: value?.note_links || [],
|
||||||
};
|
};
|
||||||
@@ -138,70 +128,10 @@ const stepClass = (status) => {
|
|||||||
const saveTask = () => {
|
const saveTask = () => {
|
||||||
emit("save-task", {
|
emit("save-task", {
|
||||||
...localTask.value,
|
...localTask.value,
|
||||||
category_id: localTask.value.category_id || null,
|
task_list_id: localTask.value.task_list_id || null,
|
||||||
parent_task_id: localTask.value.parent_task_id || null,
|
parent_task_id: localTask.value.parent_task_id || null,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="../assets/styles/scoped/components/TaskDetailModal.css"></style>
|
||||||
.status-progress {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.45rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-step {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.45rem;
|
|
||||||
color: #627086;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-step.current {
|
|
||||||
color: #0f172a;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-step.done {
|
|
||||||
color: #1f7a4d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 999px;
|
|
||||||
border: 2px solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtask-row {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 0.35rem;
|
|
||||||
border: 1px solid #dbe4f0;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #f8fbff;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.35rem 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Dark mode ── */
|
|
||||||
:root[data-bs-theme="dark"] .progress-step {
|
|
||||||
color: #7a8fa8;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .progress-step.current {
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .progress-step.done {
|
|
||||||
color: #4ade80;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .subtask-row {
|
|
||||||
background: #252b38;
|
|
||||||
border-color: #3a3f4b;
|
|
||||||
color: #c8d3e6;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
80
frontend/src/components/WorkspaceList.vue
Normal file
80
frontend/src/components/WorkspaceList.vue
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<div class="workspace-list" :class="{ 'workspace-list--list': viewMode === 'list' }">
|
||||||
|
<div v-if="items.length === 0" class="empty-workspace-state" role="status" aria-live="polite">
|
||||||
|
<i class="mdi mdi-view-grid-outline empty-workspace-icon" aria-hidden="true"></i>
|
||||||
|
<h3 class="empty-workspace-title">Nothing Here Yet</h3>
|
||||||
|
<p class="empty-workspace-message">This view has no notes or task lists yet.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="item in items" :key="`${item.kind}-${item.id}`" class="content-card" :class="contentCardClass(item)" @click="openItem(item)">
|
||||||
|
<h5 class="content-title">
|
||||||
|
<template v-if="item.kind === 'note'">
|
||||||
|
<i v-if="item.is_pinned" class="mdi mdi-pin pin-icon" aria-hidden="true"></i>
|
||||||
|
<i v-else-if="item.is_favorite || item.is_featured" class="mdi mdi-star featured-icon" aria-hidden="true"></i>
|
||||||
|
{{ item.title }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<i class="mdi mdi-format-list-checkbox list-icon" aria-hidden="true"></i>
|
||||||
|
{{ item.name }}
|
||||||
|
</template>
|
||||||
|
</h5>
|
||||||
|
<p class="content-preview">{{ getDescription(item) }}</p>
|
||||||
|
<small class="text-muted">Updated: {{ formatDate(item.updated_at) }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="canLoadMore" class="list-footer">
|
||||||
|
<button class="btn btn-outline-secondary" :disabled="isLoadingMore" @click.stop="emit('loadMore')">
|
||||||
|
{{ isLoadingMore ? "Loading..." : "Load more" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
canLoadMore: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isLoadingMore: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
viewMode: {
|
||||||
|
type: String,
|
||||||
|
default: "grid",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["selectNote", "selectTaskList", "loadMore"]);
|
||||||
|
|
||||||
|
const openItem = (item) => {
|
||||||
|
if (item.kind === "task-list") {
|
||||||
|
emit("selectTaskList", item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit("selectNote", item);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString) => new Date(dateString).toLocaleDateString();
|
||||||
|
|
||||||
|
const getDescription = (item) => {
|
||||||
|
const description = (item?.description || "").trim();
|
||||||
|
if (description) {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
return item.kind === "task-list" ? "Open this task list to manage tasks." : "No description";
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentCardClass = (item) => ({
|
||||||
|
"is-pinned": item.kind === "note" && item.is_pinned,
|
||||||
|
"is-featured": item.kind === "note" && (item.is_favorite || item.is_featured),
|
||||||
|
"is-task-list": item.kind === "task-list",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped src="../assets/styles/scoped/components/WorkspaceList.css"></style>
|
||||||
@@ -718,237 +718,4 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="../assets/styles/scoped/pages/Admin.css"></style>
|
||||||
.admin-page {
|
|
||||||
width: 100%;
|
|
||||||
max-width: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-topbar {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 1rem;
|
|
||||||
border-bottom: 1px solid #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-shell {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
gap: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-sidebar {
|
|
||||||
width: 280px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-right: 1px solid #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-sidebar-inner {
|
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-nav .nav-link {
|
|
||||||
border-radius: 0.6rem;
|
|
||||||
color: #495057;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-nav .nav-link:hover {
|
|
||||||
background: #eef2f7;
|
|
||||||
color: #212529;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-nav .nav-link.active {
|
|
||||||
background: #212529;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-content {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-section {
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.users-list .list-group-item {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-row-main {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-row-actions {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-actions-stack {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-name-line {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 0.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-name {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-meta-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 0.75rem 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-meta-label {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
color: #6c757d;
|
|
||||||
margin-bottom: 0.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-meta-value {
|
|
||||||
color: #495057;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-meta-item-groups {
|
|
||||||
grid-column: span 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 991.98px) {
|
|
||||||
.user-meta-grid {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-meta-item-groups {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
|
||||||
.admin-shell {
|
|
||||||
display: block;
|
|
||||||
min-height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-topbar {
|
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-content {
|
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-sidebar-backdrop {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.45);
|
|
||||||
z-index: 1400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-sidebar {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: min(82vw, 320px);
|
|
||||||
z-index: 1410;
|
|
||||||
transform: translateX(-100%);
|
|
||||||
transition: transform 0.25s ease;
|
|
||||||
border-right: 1px solid #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-sidebar-inner {
|
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-sidebar.open {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-row {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-row-actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-row-actions .btn {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-actions-stack {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-meta-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 0.65rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode overrides */
|
|
||||||
:root[data-bs-theme="dark"] .admin-topbar {
|
|
||||||
border-bottom-color: #3a3f4b;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .admin-sidebar {
|
|
||||||
background: #21252e;
|
|
||||||
border-right-color: #3a3f4b;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .admin-nav .nav-link {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .admin-nav .nav-link:hover {
|
|
||||||
background: #2d3748;
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .admin-nav .nav-link.active {
|
|
||||||
background: #e2e8f0;
|
|
||||||
color: #1a1d23;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .user-meta-value {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-bs-theme="dark"] .admin-section {
|
|
||||||
background-color: #21252e;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -142,119 +142,7 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="../assets/styles/scoped/pages/Login.css"></style>
|
||||||
.login-page {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 1.25rem;
|
|
||||||
background: radial-gradient(circle at 10% 10%, rgba(255, 255, 255, 0.2), transparent 45%), linear-gradient(135deg, #3554d1 0%, #4f46a5 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 460px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card {
|
|
||||||
background: #fff;
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 18px;
|
|
||||||
box-shadow: 0 22px 48px rgba(16, 24, 40, 0.22);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-block {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-mark {
|
|
||||||
width: 42px;
|
|
||||||
height: 42px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: rgba(53, 84, 209, 0.12);
|
|
||||||
color: #2f4ac1;
|
|
||||||
font-size: 1.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 2.05rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
color: #2f3237;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-title {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 2.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
color: #2f3237;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
border-radius: 0.65rem;
|
|
||||||
min-height: 48px;
|
|
||||||
border-color: #d6dbe4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-submit {
|
|
||||||
min-height: 48px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-provider-btn {
|
|
||||||
min-height: 48px;
|
|
||||||
border-radius: 0.65rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oauth-divider {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: #6c757d;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oauth-divider::before,
|
|
||||||
.oauth-divider::after {
|
|
||||||
content: "";
|
|
||||||
flex: 1;
|
|
||||||
border-bottom: 1px solid #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oauth-divider span {
|
|
||||||
padding: 0 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-switch-link {
|
|
||||||
color: #4b5565;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
.login-page {
|
|
||||||
padding: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card {
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 1.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-title {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-title {
|
|
||||||
font-size: 1.85rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -322,74 +322,7 @@ watch(
|
|||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="../assets/styles/scoped/pages/PublicSpace.css"></style>
|
||||||
.public-layout {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-body {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-sidebar {
|
|
||||||
width: 280px;
|
|
||||||
overflow-y: auto;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-item {
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
transition: background 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-item:hover {
|
|
||||||
background: #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-item.active {
|
|
||||||
background: #dbe4ff;
|
|
||||||
border-color: #748ffc;
|
|
||||||
color: #364fc7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-item.is-featured {
|
|
||||||
background: #fff4e6;
|
|
||||||
border-color: #ffd8a8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-item.is-featured:hover {
|
|
||||||
background: #ffe8cc;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.public-sidebar-backdrop {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
z-index: 1090;
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-sidebar {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 1095;
|
|
||||||
transform: translateX(-100%);
|
|
||||||
transition: transform 0.3s ease-in-out;
|
|
||||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-sidebar.open {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -110,96 +110,7 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped src="../assets/styles/scoped/pages/Register.css"></style>
|
||||||
.register-page {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 1.25rem;
|
|
||||||
background: radial-gradient(circle at 10% 10%, rgba(255, 255, 255, 0.2), transparent 45%), linear-gradient(135deg, #3554d1 0%, #4f46a5 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 560px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.register-card {
|
|
||||||
background: #fff;
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 18px;
|
|
||||||
box-shadow: 0 22px 48px rgba(16, 24, 40, 0.22);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-block {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-mark {
|
|
||||||
width: 42px;
|
|
||||||
height: 42px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: rgba(53, 84, 209, 0.12);
|
|
||||||
color: #2f4ac1;
|
|
||||||
font-size: 1.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 2.05rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
color: #2f3237;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-title {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 2.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
color: #2f3237;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
border-radius: 0.65rem;
|
|
||||||
min-height: 48px;
|
|
||||||
border-color: #d6dbe4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-submit {
|
|
||||||
min-height: 48px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-switch-link {
|
|
||||||
color: #4b5565;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
.register-page {
|
|
||||||
padding: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.register-card {
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 1.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-title {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-title {
|
|
||||||
font-size: 1.85rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -13,12 +13,13 @@ export const useSpaceStore = defineStore("space", () => {
|
|||||||
const notesLoading = ref(false);
|
const notesLoading = ref(false);
|
||||||
const categories = ref([]);
|
const categories = ref([]);
|
||||||
const categoryTree = ref([]);
|
const categoryTree = ref([]);
|
||||||
|
const taskLists = ref([]);
|
||||||
const tasks = ref([]);
|
const tasks = ref([]);
|
||||||
const taskStatuses = ref([]);
|
const taskStatuses = ref([]);
|
||||||
const noteLinkedTasks = ref([]);
|
const noteLinkedTasks = ref([]);
|
||||||
|
|
||||||
const refreshSpaceData = async (spaceId) => {
|
const refreshSpaceData = async (spaceId) => {
|
||||||
await Promise.all([fetchCategories(spaceId), fetchNotes(spaceId), fetchTaskStatuses(spaceId), fetchTasks(spaceId)]);
|
await Promise.all([fetchCategories(spaceId), fetchNotes(spaceId), fetchTaskLists(spaceId), fetchTaskStatuses(spaceId), fetchTasks(spaceId)]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchSpaces = async () => {
|
const fetchSpaces = async () => {
|
||||||
@@ -227,6 +228,39 @@ export const useSpaceStore = defineStore("space", () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchTaskLists = async (spaceId) => {
|
||||||
|
if (!spaceId) {
|
||||||
|
taskLists.value = [];
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(`/api/v1/spaces/${spaceId}/task-lists`);
|
||||||
|
taskLists.value = response.data || [];
|
||||||
|
return taskLists.value;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching task lists:", error);
|
||||||
|
taskLists.value = [];
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTaskList = async (spaceId, payload) => {
|
||||||
|
const response = await apiClient.post(`/api/v1/spaces/${spaceId}/task-lists`, payload);
|
||||||
|
await fetchTaskLists(spaceId);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTaskList = async (spaceId, taskListId, payload) => {
|
||||||
|
const response = await apiClient.put(`/api/v1/spaces/${spaceId}/task-lists/${taskListId}`, payload);
|
||||||
|
await fetchTaskLists(spaceId);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTaskList = async (spaceId, taskListId) => {
|
||||||
|
await apiClient.delete(`/api/v1/spaces/${spaceId}/task-lists/${taskListId}`);
|
||||||
|
await fetchTaskLists(spaceId);
|
||||||
|
};
|
||||||
|
|
||||||
const createTaskStatus = async (spaceId, payload) => {
|
const createTaskStatus = async (spaceId, payload) => {
|
||||||
const response = await apiClient.post(`/api/v1/spaces/${spaceId}/task-statuses`, payload);
|
const response = await apiClient.post(`/api/v1/spaces/${spaceId}/task-statuses`, payload);
|
||||||
await fetchTaskStatuses(spaceId);
|
await fetchTaskStatuses(spaceId);
|
||||||
@@ -258,8 +292,8 @@ export const useSpaceStore = defineStore("space", () => {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const params = {};
|
const params = {};
|
||||||
if (filters.categoryId) {
|
if (filters.taskListId) {
|
||||||
params.categoryId = filters.categoryId;
|
params.taskListId = filters.taskListId;
|
||||||
}
|
}
|
||||||
if (filters.statusId) {
|
if (filters.statusId) {
|
||||||
params.statusId = filters.statusId;
|
params.statusId = filters.statusId;
|
||||||
@@ -344,6 +378,7 @@ export const useSpaceStore = defineStore("space", () => {
|
|||||||
notesLoading,
|
notesLoading,
|
||||||
categories,
|
categories,
|
||||||
categoryTree,
|
categoryTree,
|
||||||
|
taskLists,
|
||||||
tasks,
|
tasks,
|
||||||
taskStatuses,
|
taskStatuses,
|
||||||
noteLinkedTasks,
|
noteLinkedTasks,
|
||||||
@@ -363,6 +398,10 @@ export const useSpaceStore = defineStore("space", () => {
|
|||||||
searchNotes,
|
searchNotes,
|
||||||
clearSearchResults,
|
clearSearchResults,
|
||||||
fetchTaskStatuses,
|
fetchTaskStatuses,
|
||||||
|
fetchTaskLists,
|
||||||
|
createTaskList,
|
||||||
|
updateTaskList,
|
||||||
|
deleteTaskList,
|
||||||
createTaskStatus,
|
createTaskStatus,
|
||||||
updateTaskStatus,
|
updateTaskStatus,
|
||||||
deleteTaskStatus,
|
deleteTaskStatus,
|
||||||
|
|||||||
Reference in New Issue
Block a user