diff --git a/server/mergin/sync/commands.py b/server/mergin/sync/commands.py
index 882b73b6..4262e60f 100644
--- a/server/mergin/sync/commands.py
+++ b/server/mergin/sync/commands.py
@@ -123,9 +123,7 @@ def remove(project_name):
if not project:
click.secho("ERROR: Project does not exist", fg="red", err=True)
sys.exit(1)
- project.removed_at = datetime.utcnow()
- project.removed_by = None
- db.session.commit()
+ project.schedule_deletion()
click.secho("Project removed", fg="green")
@project.command()
diff --git a/server/mergin/sync/models.py b/server/mergin/sync/models.py
index 3b32cda9..3817acd6 100644
--- a/server/mergin/sync/models.py
+++ b/server/mergin/sync/models.py
@@ -58,6 +58,7 @@
Storages = {"local": DiskStorage}
project_deleted = signal("project_deleted")
+project_soft_deleted = signal("project_soft_deleted")
project_access_granted = signal("project_access_granted")
push_finished = signal("push_finished")
project_version_created = signal("project_version_created")
@@ -295,6 +296,16 @@ def expiration(self) -> timedelta:
initial = timedelta(days=current_app.config["DELETED_PROJECT_EXPIRATION"])
return initial - (datetime.utcnow() - self.removed_at)
+ def schedule_deletion(self, removed_by: int = None):
+ """Schedule project for removal (soft-delete).
+ Sets removed_at so the project is hidden from users but kept in db
+ until a background job permanently deletes it.
+ """
+ self.removed_at = datetime.utcnow()
+ self.removed_by = removed_by
+ db.session.commit()
+ project_soft_deleted.send(self)
+
def delete(self, removed_by: int = None):
"""Mark project as permanently deleted (but keep in db)
- rename (to free up the same name)
diff --git a/server/mergin/sync/public_api_controller.py b/server/mergin/sync/public_api_controller.py
index 8dbe1237..34a2d28f 100644
--- a/server/mergin/sync/public_api_controller.py
+++ b/server/mergin/sync/public_api_controller.py
@@ -285,9 +285,7 @@ def delete_project(namespace, project_name): # noqa: E501
:rtype: None
"""
project = require_project(namespace, project_name, ProjectPermissions.Delete)
- project.removed_at = datetime.utcnow()
- project.removed_by = current_user.id
- db.session.commit()
+ project.schedule_deletion(removed_by=current_user.id)
return NoContent, 200
diff --git a/server/mergin/sync/public_api_v2_controller.py b/server/mergin/sync/public_api_v2_controller.py
index ebd909ad..e7806865 100644
--- a/server/mergin/sync/public_api_v2_controller.py
+++ b/server/mergin/sync/public_api_v2_controller.py
@@ -76,9 +76,7 @@ def schedule_delete_project(id):
rest.
"""
project = require_project_by_uuid(id, ProjectPermissions.Delete)
- project.removed_at = datetime.utcnow()
- project.removed_by = current_user.id
- db.session.commit()
+ project.schedule_deletion(removed_by=current_user.id)
return NoContent, 204
diff --git a/server/mergin/tests/test_project_controller.py b/server/mergin/tests/test_project_controller.py
index d1f1afd6..1a0c76aa 100644
--- a/server/mergin/tests/test_project_controller.py
+++ b/server/mergin/tests/test_project_controller.py
@@ -37,6 +37,7 @@
FileHistory,
PushChangeType,
ProjectFilePath,
+ project_soft_deleted,
)
from ..sync.storages.disk import copy_file as real_copy_file
from ..sync.files import files_changes_from_upload
@@ -2571,9 +2572,28 @@ def test_signals(client):
project = create_project("test-project", workspace, user)
with patch(
"mergin.sync.public_api_controller.push_finished.send"
- ) as push_finished_mock:
+ ) as push_finished_mock, patch(
+ "mergin.sync.public_api_controller.project_version_created.send"
+ ) as project_version_created_mock:
upload_file_to_project(project, "test.txt", client)
push_finished_mock.assert_called_once()
+ project_version_created_mock.assert_called_once()
+
+
+def test_project_soft_delete(client):
+ """project.schedule_deletion() sets removed_at/removed_by and fires project_soft_deleted signal"""
+ workspace = create_workspace()
+ user = User.query.filter_by(username="mergin").first()
+ project = create_project("remove-test", workspace, user)
+
+ with patch("mergin.sync.models.project_soft_deleted.send") as signal_mock:
+ project.schedule_deletion(removed_by=user.id)
+ signal_mock.assert_called_once_with(project)
+
+ assert project.removed_at is not None
+ assert project.removed_by == user.id
+ # project storage params is still present (soft delete)
+ assert project.storage_params is not None
def test_filepath_manipulation(client):
diff --git a/web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue b/web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue
index ce54d755..c779d9e2 100644
--- a/web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue
+++ b/web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue
@@ -19,7 +19,7 @@
shape="circle"
:pt="{
root: {
- class: 'font-semibold text-color-forest'
+ class: 'font-semibold'
}
}"
/>
diff --git a/web-app/packages/admin-lib/src/modules/admin/views/ProjectView.vue b/web-app/packages/admin-lib/src/modules/admin/views/ProjectView.vue
index 4a975d9a..255e07cb 100644
--- a/web-app/packages/admin-lib/src/modules/admin/views/ProjectView.vue
+++ b/web-app/packages/admin-lib/src/modules/admin/views/ProjectView.vue
@@ -6,6 +6,7 @@
Project details
+
-
+
-
-
- {{
- userName
- }}
-
- {{ currentWorkspace?.name || 'no workspace' }}
-
-
-
+
+
+
+ {{
+ userName
+ }}
+
+ {{ currentWorkspace?.name || 'no workspace' }}
+
+
+
+
+
@@ -129,15 +160,14 @@ import { defineComponent, ref, PropType } from 'vue'
import { AppBreadcrumbs } from '.'
-import { AppMenu, UserRouteName, useInstanceStore } from '@/main'
+import { UserRouteName, useInstanceStore } from '@/main'
import { useLayoutStore } from '@/modules/layout/store'
import { useUserStore } from '@/modules/user/store'
export default defineComponent({
name: 'app-header-template',
components: {
- AppBreadcrumbs,
- AppMenu
+ AppBreadcrumbs
},
props: {
renderNamespace: {
@@ -153,14 +183,23 @@ export default defineComponent({
},
setup() {
const menu = ref()
+ const menuHelp = ref()
+ const menuVisible = ref(false)
const toggleMenu = (event) => {
menu.value.toggle(event)
}
+ const toggleMenuHelp = (event) => {
+ menuHelp.value.toggle(event)
+ }
+
return {
menu,
- toggleMenu
+ menuHelp,
+ menuVisible,
+ toggleMenu,
+ toggleMenuHelp
}
},
computed: {
@@ -215,6 +254,11 @@ export default defineComponent({
? `${this.getUserFullName.substring(0, 15)}...`
: this.getUserFullName
},
+ hasSidebar() {
+ return this.$route.matched.some(
+ (record) => record.components != null && 'sidebar' in record.components
+ )
+ },
menuButtonIcon() {
if (this.isUnderOverlayBreakpoint) {
return 'ti ti-menu-2'
diff --git a/web-app/packages/lib/src/modules/project/components/FileDetailSidebar.vue b/web-app/packages/lib/src/modules/project/components/FileDetailSidebar.vue
index 4dcec5ee..b73cefbd 100644
--- a/web-app/packages/lib/src/modules/project/components/FileDetailSidebar.vue
+++ b/web-app/packages/lib/src/modules/project/components/FileDetailSidebar.vue
@@ -126,7 +126,7 @@ export default defineComponent({
},
file() {
return (
- this.project?.files[this.filePath] || this.upload?.files[this.filePath]
+ this.project?.files?.[this.filePath] || this.upload?.files?.[this.filePath]
)
},
fileName() {
diff --git a/web-app/packages/lib/src/modules/project/components/FilesTable.vue b/web-app/packages/lib/src/modules/project/components/FilesTable.vue
index 1409ccd7..8b8de543 100644
--- a/web-app/packages/lib/src/modules/project/components/FilesTable.vue
+++ b/web-app/packages/lib/src/modules/project/components/FilesTable.vue
@@ -181,7 +181,7 @@ const breadcrumps = computed(() => {
})
const projectFiles = computed(() => {
- let files = projectStore.project.files
+ let files = projectStore.project.files ?? []
if (projectStore.uploads[projectStore.project.path] && diff.value) {
files = {
...files,
diff --git a/web-app/packages/lib/src/modules/project/components/ProjectShareDialog.vue b/web-app/packages/lib/src/modules/project/components/ProjectShareDialog.vue
index 9fac8ecc..04298c28 100644
--- a/web-app/packages/lib/src/modules/project/components/ProjectShareDialog.vue
+++ b/web-app/packages/lib/src/modules/project/components/ProjectShareDialog.vue
@@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
shape="circle"
:pt="{
root: {
- class: 'font-semibold text-color-forest mr-2',
+ class: 'font-semibold mr-2',
style: {
borderRadius: '50%'
}
diff --git a/web-app/packages/lib/src/modules/project/projectApi.ts b/web-app/packages/lib/src/modules/project/projectApi.ts
index 657675c8..c17e4d5b 100644
--- a/web-app/packages/lib/src/modules/project/projectApi.ts
+++ b/web-app/packages/lib/src/modules/project/projectApi.ts
@@ -28,7 +28,8 @@ import {
UpdateProjectCollaboratorPayload,
UpdatePublicFlagParams,
ProjectCollaborator,
- AddProjectCollaboratorPayload
+ AddProjectCollaboratorPayload,
+ ProjectV2Response
} from '@/modules/project/types'
export const ProjectApi = {
@@ -39,6 +40,10 @@ export const ProjectApi = {
return ProjectModule.httpService(`/v1/project/${namespace}/${projectName}`)
},
+ async getProjectV2(id: string): Promise> {
+ return ProjectModule.httpService(`/v2/projects/${id}`)
+ },
+
async createProject(
namespace: string,
data: CreateProjectParams,
diff --git a/web-app/packages/lib/src/modules/project/store.ts b/web-app/packages/lib/src/modules/project/store.ts
index 1edf1ae7..302b9a72 100644
--- a/web-app/packages/lib/src/modules/project/store.ts
+++ b/web-app/packages/lib/src/modules/project/store.ts
@@ -45,7 +45,8 @@ import {
ProjectVersionListItem,
UpdateProjectCollaboratorPayload,
UpdatePublicFlagParams,
- ProjectCollaborator
+ ProjectCollaborator,
+ ProjectV2Response
} from '@/modules/project/types'
import { useUserStore } from '@/modules/user/store'
@@ -170,6 +171,29 @@ export const useProjectStore = defineStore('projectModule', {
})
}
},
+ setProjectV2(payload: { project: ProjectV2Response }) {
+ this.project = {
+ ...(this.project ?? {}),
+ // Transofrm ProjectV2Response to EnhancedProjectDetail , we can skip files
+ id: payload.project.id,
+ name: payload.project.name,
+ namespace: payload.project.workspace.name,
+ workspace_id: payload.project.workspace.id,
+ role: payload.project.role,
+ path: [payload.project.workspace.name, payload.project.name].join('/'),
+ created: payload.project.created_at,
+ updated: payload.project.updated_at,
+ access: undefined,
+ version: payload.project.version,
+ // Leave as undefined in this time. When full implementation needed, we can use it.
+ files: undefined,
+ creator: undefined,
+ disk_usage: payload.project.size,
+ has_conflict: undefined,
+ permissions: undefined,
+ tags: undefined
+ }
+ },
setProjects(payload: ProjectsPayload) {
this.projects = payload.projects
this.projectsCount = payload.count
@@ -489,6 +513,17 @@ export const useProjectStore = defineStore('projectModule', {
}
},
+ async getProjectV2(projectId: string) {
+ const notificationStore = useNotificationStore()
+
+ try {
+ const projectResponse = await ProjectApi.getProjectV2(projectId)
+ this.setProjectV2({ project: projectResponse.data })
+ } catch {
+ await notificationStore.error({ text: 'Failed to load project data' })
+ }
+ },
+
async unsubscribeProject(payload) {
const notificationStore = useNotificationStore()
diff --git a/web-app/packages/lib/src/modules/project/types.ts b/web-app/packages/lib/src/modules/project/types.ts
index e50e0492..169a4bfc 100644
--- a/web-app/packages/lib/src/modules/project/types.ts
+++ b/web-app/packages/lib/src/modules/project/types.ts
@@ -57,6 +57,21 @@ export interface Project {
created?: string
}
+export interface ProjectV2Response {
+ id: string
+ name: string
+ version: string
+ public: boolean
+ size: number
+ created_at: string
+ updated_at: string
+ workspace: {
+ id: number
+ name: string
+ }
+ role: ProjectRoleName | null
+}
+
export interface ProjectListItem extends Project {
access: ProjectAccess
creator: number | null
diff --git a/web-app/packages/lib/src/modules/project/views/FileVersionDetailView.vue b/web-app/packages/lib/src/modules/project/views/FileVersionDetailView.vue
index a41c7f32..403d229c 100644
--- a/web-app/packages/lib/src/modules/project/views/FileVersionDetailView.vue
+++ b/web-app/packages/lib/src/modules/project/views/FileVersionDetailView.vue
@@ -74,8 +74,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
No changeset for current layer
- console.log('getChangeset', this.namespace, this.projectName,
- this.version_id, this.path)
diff --git a/web-app/packages/lib/src/modules/user/views/ProfileViewTemplate.vue b/web-app/packages/lib/src/modules/user/views/ProfileViewTemplate.vue
index 86191186..136ee323 100644
--- a/web-app/packages/lib/src/modules/user/views/ProfileViewTemplate.vue
+++ b/web-app/packages/lib/src/modules/user/views/ProfileViewTemplate.vue
@@ -64,7 +64,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
size="xlarge"
:pt="{
root: {
- class: 'font-semibold text-color-forest'
+ class: 'font-semibold'
}
}"
/>