Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
295e03feb4 | ||
|
|
b09137eca5 |
@@ -10,8 +10,6 @@ JWT_SECRET=your-super-secret-jwt-key-minimum-32-characters-change-in-production
|
|||||||
ENCRYPTION_KEY=A5CC60AB92FCA026F5477DC486555882
|
ENCRYPTION_KEY=A5CC60AB92FCA026F5477DC486555882
|
||||||
FRONTEND_URL="http://localhost"
|
FRONTEND_URL="http://localhost"
|
||||||
|
|
||||||
VITE_API_BASE_URL="http://localhost"
|
|
||||||
|
|
||||||
# Default Admin
|
# Default Admin
|
||||||
DEFAULT_ADMIN_EMAIL=admin@notely.local
|
DEFAULT_ADMIN_EMAIL=admin@notely.local
|
||||||
DEFAULT_ADMIN_USERNAME=admin
|
DEFAULT_ADMIN_USERNAME=admin
|
||||||
|
|||||||
@@ -46,8 +46,6 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
file: ./devops/docker/Dockerfile
|
file: ./devops/docker/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
build-args: |
|
|
||||||
VITE_API_BASE_URL=${{ secrets.VITE_API_BASE_URL }}
|
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.IMAGE_NAME }}:latest
|
${{ env.IMAGE_NAME }}:latest
|
||||||
${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.short_sha }}
|
${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.short_sha }}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ Required or commonly used:
|
|||||||
- `JWT_SECRET`
|
- `JWT_SECRET`
|
||||||
- `ENCRYPTION_KEY`
|
- `ENCRYPTION_KEY`
|
||||||
- `FRONTEND_URL`
|
- `FRONTEND_URL`
|
||||||
- `VITE_API_BASE_URL`
|
|
||||||
- `DEFAULT_ADMIN_EMAIL`
|
- `DEFAULT_ADMIN_EMAIL`
|
||||||
- `DEFAULT_ADMIN_USERNAME`
|
- `DEFAULT_ADMIN_USERNAME`
|
||||||
- `DEFAULT_ADMIN_PASSWORD`
|
- `DEFAULT_ADMIN_PASSWORD`
|
||||||
@@ -41,7 +40,6 @@ Optional backend runtime values that Docker Compose will also pass through if pr
|
|||||||
- MongoDB container: `mongodb://admin:password@mongodb:27017/noteapp?authSource=admin`
|
- MongoDB container: `mongodb://admin:password@mongodb:27017/noteapp?authSource=admin`
|
||||||
- Backend port: `8080`
|
- Backend port: `8080`
|
||||||
- Public frontend URL: `http://localhost`
|
- Public frontend URL: `http://localhost`
|
||||||
- Browser API base URL for container builds: `http://localhost`
|
|
||||||
|
|
||||||
## 2. `backend/.env`
|
## 2. `backend/.env`
|
||||||
|
|
||||||
@@ -107,13 +105,12 @@ cp .env.example .env
|
|||||||
|
|
||||||
### Frontend Variables In `frontend/.env.example`
|
### Frontend Variables In `frontend/.env.example`
|
||||||
|
|
||||||
- `VITE_API_BASE_URL`
|
|
||||||
- `VITE_ENV`
|
- `VITE_ENV`
|
||||||
- `VITE_ENABLE_ANALYTICS`
|
- `VITE_ENABLE_ANALYTICS`
|
||||||
|
|
||||||
### Variables Currently Relevant To The Frontend App
|
### Variables Currently Relevant To The Frontend App
|
||||||
|
|
||||||
- `VITE_API_BASE_URL`: used by the API client
|
- API requests are sent to the current browser origin (same-origin runtime behavior)
|
||||||
|
|
||||||
The other example values are safe to keep, but the current checked-in frontend code does not actively consume them.
|
The other example values are safe to keep, but the current checked-in frontend code does not actively consume them.
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ Check `REDIS_ADDR`, `REDIS_PASSWORD`, and `REDIS_DB`. For local defaults, Redis
|
|||||||
Check:
|
Check:
|
||||||
|
|
||||||
- backend is running on port `8080`
|
- backend is running on port `8080`
|
||||||
- frontend `VITE_API_BASE_URL`
|
- frontend and API are reachable through the same host/origin
|
||||||
- Vite proxy settings in `frontend/vite.config.js`
|
- Vite proxy settings in `frontend/vite.config.js`
|
||||||
|
|
||||||
### OAuth callback redirects to the wrong URL
|
### OAuth callback redirects to the wrong URL
|
||||||
|
|||||||
@@ -812,13 +812,9 @@ func (s *TaskService) DeleteTaskList(ctx context.Context, spaceID, taskListID, u
|
|||||||
return errors.New("task list not found")
|
return errors.New("task list not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks, err := s.taskRepo.ListTasks(ctx, spaceID, map[string]any{"task_list_id": taskListID})
|
if err := s.taskRepo.DeleteTasksByTaskListID(ctx, taskListID); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(tasks) > 0 {
|
|
||||||
return errors.New("cannot delete task list with tasks")
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.taskListRepo.DeleteTaskList(ctx, taskListID)
|
return s.taskListRepo.DeleteTaskList(ctx, taskListID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ FROM node:25-alpine AS frontend-builder
|
|||||||
|
|
||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
|
|
||||||
ARG VITE_API_BASE_URL
|
|
||||||
ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
|
|
||||||
|
|
||||||
COPY frontend/package*.json ./
|
COPY frontend/package*.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./devops/docker/Dockerfile
|
dockerfile: ./devops/docker/Dockerfile
|
||||||
args:
|
|
||||||
VITE_API_BASE_URL: ${VITE_API_BASE_URL}
|
|
||||||
container_name: notely-app
|
container_name: notely-app
|
||||||
ports:
|
ports:
|
||||||
- "${BACKEND_PORT}:${BACKEND_PORT}"
|
- "${BACKEND_PORT}:${BACKEND_PORT}"
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
# Frontend Environment Example
|
# Frontend Environment Example
|
||||||
|
|
||||||
# API Base URL (Backend server)
|
|
||||||
VITE_API_BASE_URL=http://localhost:8080
|
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
VITE_ENV=development
|
VITE_ENV=development
|
||||||
|
|
||||||
|
|||||||
@@ -212,6 +212,8 @@
|
|||||||
v-if="activeView === 'tasks'"
|
v-if="activeView === 'tasks'"
|
||||||
:tasks="tasks"
|
:tasks="tasks"
|
||||||
:statuses="taskStatuses"
|
:statuses="taskStatuses"
|
||||||
|
:selected-task-list="selectedTaskList"
|
||||||
|
:can-delete-task-list="canDeleteTasks"
|
||||||
@select-task="openTaskDetail"
|
@select-task="openTaskDetail"
|
||||||
@filter-change="applyTaskFilters"
|
@filter-change="applyTaskFilters"
|
||||||
@reorder-status="reorderTaskStatuses"
|
@reorder-status="reorderTaskStatuses"
|
||||||
@@ -219,6 +221,7 @@
|
|||||||
@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"
|
||||||
@@ -1294,6 +1297,36 @@ const createTaskList = async (taskListData) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
|||||||
@@ -185,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">
|
||||||
@@ -235,9 +241,17 @@ const props = defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
selectedTaskList: {
|
||||||
|
type: Object,
|
||||||
|
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 filterStatus = ref("");
|
const filterStatus = ref("");
|
||||||
const filterParent = ref("");
|
const filterParent = ref("");
|
||||||
@@ -470,6 +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 src="../assets/styles/scoped/components/TaskBoard.css"></style>
|
<style scoped src="../assets/styles/scoped/components/TaskBoard.css"></style>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useAuthStore } from "../stores/authStore";
|
import { useAuthStore } from "../stores/authStore";
|
||||||
|
|
||||||
|
const runtimeOrigin = typeof window !== "undefined" ? window.location.origin : "";
|
||||||
|
|
||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_BASE_URL || "http://localhost:8080",
|
baseURL: runtimeOrigin,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user