diff --git a/frontend/src/components/home/Navigation.vue b/frontend/src/components/home/Navigation.vue index e8508c633..7534b9e4d 100644 --- a/frontend/src/components/home/Navigation.vue +++ b/frontend/src/components/home/Navigation.vue @@ -71,6 +71,16 @@ {{ $t('team.title') }} +
  • + + + + + {{ $t('project.bin.title') }} + +
  • diff --git a/frontend/src/i18n/lang/en.json b/frontend/src/i18n/lang/en.json index 4e7459760..074a161f3 100644 --- a/frontend/src/i18n/lang/en.json +++ b/frontend/src/i18n/lang/en.json @@ -322,12 +322,22 @@ "delete": { "title": "Delete \"{project}\"", "header": "Delete this project", - "text1": "Are you sure you want to delete this project and all of its contents?", - "text2": "This includes all tasks and CANNOT BE UNDONE!", - "success": "The project was successfully deleted.", - "tasksToDelete": "This will irrevocably remove approx. {count} tasks.", - "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", - "noTasksToDelete": "This project does not contain any tasks, it should be safe to delete." + "text1": "Are you sure you want to move this project to the bin?", + "text2": "It will be permanently deleted after 30 days.", + "success": "The project was moved to the bin.", + "tasksToDelete": "This will affect approx. {count} tasks.", + "tasksAndChildProjectsToDelete": "This will affect approx. {tasks} tasks and {projects} projects.", + "noTasksToDelete": "This project does not contain any tasks, it should be safe to delete.", + "undoSuccess": "The project was restored.", + "undo": "Undo" + }, + "bin": { + "title": "Bin", + "empty": "No deleted projects.", + "daysRemaining": "{days} days remaining", + "deletedOn": "Deleted on {date}", + "restore": "Restore", + "restoreSuccess": "The project was restored successfully." }, "duplicate": { "title": "Duplicate this project", diff --git a/frontend/src/modelTypes/IProject.ts b/frontend/src/modelTypes/IProject.ts index 555b35a97..33eb35151 100644 --- a/frontend/src/modelTypes/IProject.ts +++ b/frontend/src/modelTypes/IProject.ts @@ -21,7 +21,8 @@ export interface IProject extends IAbstract { backgroundBlurHash: string parentProjectId: number views: IProjectView[] - + deletedAt: Date | null + created: Date updated: Date } diff --git a/frontend/src/models/project.ts b/frontend/src/models/project.ts index 53b75a9b8..c685817c4 100644 --- a/frontend/src/models/project.ts +++ b/frontend/src/models/project.ts @@ -26,7 +26,8 @@ export default class ProjectModel extends AbstractModel implements IPr backgroundBlurHash = '' parentProjectId = 0 views: IProjectView[] = [] - + deletedAt: Date | null = null + created: Date = null updated: Date = null @@ -51,6 +52,10 @@ export default class ProjectModel extends AbstractModel implements IPr this.views = this.views.map(v => new ProjectViewModel(v)) + if (this.deletedAt) { + this.deletedAt = new Date(this.deletedAt) + } + this.created = new Date(this.created) this.updated = new Date(this.updated) } diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 1f4b15121..42efd02f3 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -212,6 +212,11 @@ const router = createRouter({ name: 'projects.index', component: () => import('@/views/project/ListProjects.vue'), }, + { + path: '/projects/bin', + name: 'projects.bin', + component: () => import('@/views/project/ProjectsBin.vue'), + }, { path: '/projects/new', name: 'project.create', diff --git a/frontend/src/services/project.ts b/frontend/src/services/project.ts index 68e828b83..a370c8a7e 100644 --- a/frontend/src/services/project.ts +++ b/frontend/src/services/project.ts @@ -52,6 +52,28 @@ export default class ProjectService extends AbstractService { return window.URL.createObjectURL(new Blob([response.data])) } + async restore(projectId: IProject['id']): Promise { + const cancel = this.setLoading() + + try { + const response = await this.http.post(`/projects/${projectId}/restore`) + return this.modelFactory(response.data) + } finally { + cancel() + } + } + + async getDeletedProjects(): Promise { + const cancel = this.setLoading() + + try { + const response = await this.http.get('/projects/deleted') + return response.data.map((p: IProject) => this.modelFactory(p)) + } finally { + cancel() + } + } + async removeBackground(project: IProject) { const cancel = this.setLoading() diff --git a/frontend/src/stores/projects.ts b/frontend/src/stores/projects.ts index 64b0d0ddd..b7b107609 100644 --- a/frontend/src/stores/projects.ts +++ b/frontend/src/stores/projects.ts @@ -257,6 +257,30 @@ export const useProjectStore = defineStore('project', () => { } } + async function restoreProject(projectId: IProject['id']) { + const cancel = setModuleLoading(setIsLoading) + const projectService = new ProjectService() + + try { + const restoredProject = await projectService.restore(projectId) + setProject(restoredProject) + return restoredProject + } finally { + cancel() + } + } + + async function fetchDeletedProjects(): Promise { + const cancel = setModuleLoading(setIsLoading) + const projectService = new ProjectService() + + try { + return await projectService.getDeletedProjects() + } finally { + cancel() + } + } + async function loadAllProjects() { const cancel = setModuleLoading(setIsLoading) @@ -353,6 +377,8 @@ export const useProjectStore = defineStore('project', () => { createProject, updateProject, deleteProject, + restoreProject, + fetchDeletedProjects, getAncestors, setProjectView, removeProjectView, diff --git a/frontend/src/views/project/ProjectsBin.vue b/frontend/src/views/project/ProjectsBin.vue new file mode 100644 index 000000000..72ca54674 --- /dev/null +++ b/frontend/src/views/project/ProjectsBin.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/frontend/src/views/project/settings/ProjectSettingsDelete.vue b/frontend/src/views/project/settings/ProjectSettingsDelete.vue index 74512eee7..b4d08e296 100644 --- a/frontend/src/views/project/settings/ProjectSettingsDelete.vue +++ b/frontend/src/views/project/settings/ProjectSettingsDelete.vue @@ -25,7 +25,7 @@ />

    - {{ $t('misc.cannotBeUndone') }} + {{ $t('project.delete.text2') }}

    @@ -88,8 +88,17 @@ async function deleteProject() { return } - await projectStore.deleteProject(project.value) - success({message: t('project.delete.success')}) + const deletedProject = project.value + await projectStore.deleteProject(deletedProject) + success({message: t('project.delete.success')}, [ + { + title: t('project.delete.undo'), + callback: async () => { + await projectStore.restoreProject(deletedProject.id) + success({message: t('project.delete.undoSuccess')}) + }, + }, + ]) router.push({name: 'home'}) }