Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions server/mergin/sync/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
11 changes: 11 additions & 0 deletions server/mergin/sync/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 1 addition & 3 deletions server/mergin/sync/public_api_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
4 changes: 1 addition & 3 deletions server/mergin/sync/public_api_v2_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 21 additions & 1 deletion server/mergin/tests/test_project_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
shape="circle"
:pt="{
root: {
class: 'font-semibold text-color-forest'
class: 'font-semibold'
}
}"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<h1 class="headline-h3">Project details</h1>
</template>
<template #headerActions>
<slot name="action-button"></slot>
<PButton
severity="secondary"
@click="downloadArchive"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ img {
color: map-get($colors, 'dark-gray');
}

.text-color-medium-gray {
color: map-get($colors, 'medium-gray');
}

.overflow-wrap-anywhere {
overflow-wrap: anywhere;
}
Expand Down Expand Up @@ -151,7 +155,11 @@ img {

// Font size in avatar
.p-avatar:not(.p-avatar-lg):not(.p-avatar-xl) {
font-size: 0.857rem;
font-size: 0.75rem;
}

.p-avatar-text {
color: map-get($map: $colors, $key: deep-ocean)
}

.tooltip {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ $colors: (
"sky": #A6CBF4,
"informative": #BEDAF0,
"dark-gray": #41464C,
"medium-gray": #A0A3A5
"medium-gray": #A0A3A5,
"secondary-gray": #E2E2E2
);

// Mandatory Designer Variables
Expand Down Expand Up @@ -119,5 +120,7 @@ $colors: (
--earth-color: #{map-get($colors, "earth")};
--dark-gray-color: #{map-get($colors, "dark-gray")};
--medium-gray-color: #{map-get($colors, "medium-gray")};
--secondary-gray-color: #{map-get($colors, "secondary-gray")};
--purple-color: #{map-get($colors, "purple")};
color-scheme: light;
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,11 @@ $progressSpinnerColorFour:$warningMessageTextColor !default;

/// Background of an avatar
/// @group misc
$avatarBg: map-get($map: $colors, $key: medium-green);
$avatarBg: map-get($map: $colors, $key: informative);

/// Text color of an avatar
/// @group misc
$avatarTextColor:$textColor;
$avatarTextColor: map-get($map: $colors, $key: deep-ocean);

/// Background of a chip
/// @group misc
Expand Down
2 changes: 1 addition & 1 deletion web-app/packages/lib/src/common/components/UserSummary.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
:size="avatarSize"
:pt="{
root: {
class: 'mr-1 text-color-forest font-semibold flex-shrink-0',
class: 'mr-1 font-semibold flex-shrink-0',
style: { borderRadius: '50%' }
}
}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
><slot name="menu">
<div class="flex flex-column lg:flex-row lg:align-items-center gap-2">
<PButton
v-if="hasSidebar"
:icon="menuButtonIcon"
plain
text
Expand All @@ -37,30 +38,40 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial

<template #end>
<div class="flex align-items-center flex-shrink-0">
<PButton
<slot name="action-button"></slot>
<slot
v-if="loggedUser"
text
plain
aria-haspopup="true"
aria-controls="app-header-profile"
data-cy="app-header-profile-btn"
@click="toggleMenu"
class="p-2 shadow-none"
name="profile-button"
:toggle-menu="toggleMenu"
:logged-user="loggedUser"
:menu-visible="menuVisible"
>
<slot name="invitationsIcon"></slot>
<div class="mr-2 max-w-80 flex flex-column align-items-start">
<span class="title-t4" :style="{ whiteSpace: 'nowrap' }">{{
userName
}}</span>
<span
v-if="renderNamespace"
class="paragraph-p6 opacity-80 font-normal"
>
{{ currentWorkspace?.name || 'no workspace' }}
</span>
</div>
<i class="ti ti-chevron-down"></i
></PButton>
<PButton
text
plain
aria-haspopup="true"
aria-controls="app-header-profile"
data-cy="app-header-profile-btn"
@click="toggleMenu"
class="p-2 shadow-none"
>
<slot name="invitationsIcon"></slot>
<div class="mr-2 max-w-80 flex flex-column align-items-end">
<span class="title-t4" :style="{ whiteSpace: 'nowrap' }">{{
userName
}}</span>
<span
v-if="renderNamespace"
class="paragraph-p6 opacity-80 font-normal"
>
{{ currentWorkspace?.name || 'no workspace' }}
</span>
</div>
<i
:class="menuVisible ? 'ti ti-chevron-up' : 'ti ti-chevron-down'"
></i>
</PButton>
</slot>
<PButton
v-else
text
Expand All @@ -77,6 +88,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
id="app-header-profile"
data-cy="app-header-profile"
ref="menu"
@show="menuVisible = true"
@hide="menuVisible = false"
:pt="{ root: { class: 'p-3' }, content: { class: 'p-0' } }"
>
<div class="flex align-items-center mb-3">
Expand All @@ -86,7 +99,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
shape="circle"
:pt="{
root: {
class: 'mr-2 text-color-forest font-semibold flex-shrink-0',
class: 'mr-2 font-semibold flex-shrink-0',
style: {
borderRadius: '50%'
}
Expand All @@ -113,12 +126,30 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
}"
></PMenu>
</POverlayPanel>
<AppMenu :items="_helpMenuItems" :icon="'ti ti-help'" />
<PButton
text
plain
rounded
icon="ti ti-help"
@click="toggleMenuHelp"
aria-haspopup="true"
aria-controls="menu-help"
class="paragraph-p4 text-color hidden md:inline-flex"
/>
<PMenu
ref="menuHelp"
id="menu-help"
:model="_helpMenuItems"
:popup="true"
/>
</div>
</template>
</PMenubar>
<!-- Show breadcrumps under menu in smaller screens -->
<app-breadcrumbs class="lg:hidden px-3 pb-3 pt-0"></app-breadcrumbs>
<!-- Show breadcrumps under menu in smaller screens (only when the default menu slot is used) -->
<app-breadcrumbs
v-if="!$slots.menu"
class="lg:hidden px-3 pb-3 pt-0"
></app-breadcrumbs>
</div>
</template>

Expand All @@ -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: {
Expand All @@ -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: {
Expand Down Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
},
file() {
return (
this.project?.files[this.filePath] || this.upload?.files[this.filePath]
this.project?.files?.[this.filePath] || this.upload?.files?.[this.filePath]

Check warning on line 129 in web-app/packages/lib/src/modules/project/components/FileDetailSidebar.vue

View workflow job for this annotation

GitHub Actions / JavaScript code convention check

Insert `⏎·······`
)
},
fileName() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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%'
}
Expand Down
Loading
Loading