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
88 changes: 88 additions & 0 deletions web-app/packages/admin-lib/src/common/components/CopyPill.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<!--
Copyright (C) Lutra Consulting Limited

SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
-->

<template>
<button
type="button"
class="copy-pill"
v-tooltip.top="'Copy to clipboard'"
:aria-label="ariaLabel"
:disabled="!value"
@click="copyValue"
>
<span>{{ value }}</span>
<i class="ti ti-copy"></i>
</button>
</template>

<script setup lang="ts">
import { useNotificationStore } from '@mergin/lib'
import { computed } from 'vue'

const props = withDefaults(
defineProps<{
/** Value shown in the pill and copied to the clipboard */
value: string | number | null | undefined
/** Human readable name of the value, used in notification and aria label */
label?: string
}>(),
{ label: 'Value' }
)

const notificationStore = useNotificationStore()

const ariaLabel = computed(() => `Copy ${props.label} ${props.value ?? ''}`)

async function copyValue() {
if (props.value === null || props.value === undefined || props.value === '') {
return
}
try {
await navigator.clipboard.writeText(String(props.value))
notificationStore.show({ text: `${props.label} copied to clipboard` })
} catch {
notificationStore.error({
text: `Failed to copy ${props.label.toLowerCase()} to clipboard`
})
}
}
</script>

<style lang="scss" scoped>
.copy-pill {
display: inline-flex;
align-items: center;
gap: 0.375rem;
margin: -0.25rem -0.5rem;
padding: 0.25rem 0.5rem;
border: none;
border-radius: 6px;
background: transparent;
color: var(--text-color);
font: inherit;
font-weight: 600;
cursor: pointer;
transition: background-color 0.15s ease;

&:hover {
background: var(--surface-hover);
}

&:disabled {
cursor: default;
opacity: 0.6;

&:hover {
background: transparent;
}
}

i {
font-size: 0.95rem;
color: inherit;
}
}
</style>
5 changes: 5 additions & 0 deletions web-app/packages/admin-lib/src/common/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright (C) Lutra Consulting Limited
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial

export { default as CopyPill } from './CopyPill.vue'
5 changes: 5 additions & 0 deletions web-app/packages/admin-lib/src/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright (C) Lutra Consulting Limited
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial

export * from './components'
1 change: 1 addition & 0 deletions web-app/packages/admin-lib/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial

export * from '@/common'
export * from '@/modules'
9 changes: 6 additions & 3 deletions web-app/packages/admin-lib/src/modules/admin/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,18 @@ export enum AdminRoutes {
Login = 'login'
}

export const getAdminTitle = (route: RouteLocationNormalizedLoaded) => {
export const getAdminTitle = (
route: RouteLocationNormalizedLoaded,
config?: { accountEmail?: string }
) => {
const params = route.params as AdminRouteParams
const titles: Record<AdminRoutes, string | string[]> = {
[AdminRoutes.Login]: ['Sign in', 'Mergin Maps Admin Panel'],
[AdminRoutes.ACCOUNTS]: 'Accounts',
[AdminRoutes.ACCOUNT]: 'Account details',
[AdminRoutes.ACCOUNT]: [config?.accountEmail || 'Account details'],
[AdminRoutes.OVERVIEW]: 'Admin overview',
[AdminRoutes.PROJECTS]: 'Projects',
[AdminRoutes.PROJECT]: ['Details', params.projectName],
[AdminRoutes.PROJECT]: [params.projectName],
[AdminRoutes.SETTINGS]: 'Settings',
[AdminRoutes.ProjectTree]: ['Files', params.projectName],
[AdminRoutes.ProjectHistory]: ['History', params.projectName],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,30 @@
<template v-if="user">
<app-container>
<app-section class="p-4">
<div
class="flex flex-column align-items-center row-gap-3 text-center"
>
<PAvatar
:label="$filters.getAvatar(user?.email, profile?.name)"
size="xlarge"
shape="circle"
:pt="{
root: {
class: 'font-semibold'
}
}"
/>
<h3 class="headline-h2" data-cy="profile-name">
{{
profile?.name
? `${profile.name} (${user?.username})`
: user.username
}}
</h3>
<div class="flex flex-column row-gap-3">
<div class="flex align-items-center gap-2">
<PAvatar
:label="$filters.getAvatar(user?.email, profile?.name)"
shape="circle"
:pt="{
root: {
class: 'font-semibold flex-shrink-0',
style: {
width: '2.25rem',
height: '2.25rem',
fontSize: '1rem'
}
}
}"
/>
<h2 class="headline-h2" data-cy="profile-name">
{{
profile?.name
? `${profile.name} (${user?.username})`
: user.username
}}
</h2>
</div>
<p
class="m-0 paragraph-p6 overflow-wrap-anywhere"
data-cy="profile-email"
Expand All @@ -45,18 +49,22 @@
{{ user?.email }}
</p>
<dl
class="profile-view-detail-list grid grid-nogutter paragraph-p5"
class="project-view-detail-list paragraph-p5 flex flex-column gap-3"
>
<div
class="col-6 flex flex-column align-items-start text-left flex-wrap"
>
<dt class="paragraph-p6 opacity-80 mb-2">Last signed in</dt>
<div>
<dt class="paragraph-p6 opacity-80">User ID</dt>
<dd class="font-semibold" data-cy="profile-id">
<copy-pill :value="user?.id" label="User ID" />
</dd>
</div>
<div>
<dt class="paragraph-p6 opacity-80">Last signed in</dt>
<dd class="font-semibold" data-cy="profile-last-signed-in">
{{ $filters.date(user.last_signed_in) || '-' }}
</dd>
</div>
<div class="col-6 flex flex-column align-items-end">
<dt class="paragraph-p6 opacity-80 mb-2">Registered</dt>
<div>
<dt class="paragraph-p6 opacity-80">Registered</dt>
<dd class="font-semibold" data-cy="profile-registered">
{{ $filters.date(user?.registration_date) }}
</dd>
Expand Down Expand Up @@ -141,10 +149,12 @@ import {
useInstanceStore,
useUserStore
} from '@mergin/lib'
import { computed, watch } from 'vue'
import { computed, watch, watchEffect } from 'vue'
import { useRoute } from 'vue-router'

import CopyPill from '@/common/components/CopyPill.vue'
import AdminLayout from '@/modules/admin/components/AdminLayout.vue'
import { getAdminTitle } from '@/modules/admin/routes'
import { useAdminStore } from '@/modules/admin/store'

const route = useRoute()
Expand Down Expand Up @@ -192,6 +202,16 @@ const fetchProfile = (username: string) => {
adminStore.fetchUserByName({ username })
}

watchEffect(() => {
// We need to handle the title separately, as the account's email is not
// available in the route and only loads after the profile is fetched
if (user.value?.email) {
document.title = getAdminTitle(route, {
accountEmail: user.value.email
})?.[0]
}
})

watch(
routeUsername,
(username) => {
Expand Down Expand Up @@ -300,7 +320,11 @@ const switchAdminAccess = async () => {
</script>

<style lang="scss" scoped>
.profile-view-detail-list {
h2 {
color: var(--text-color);
}

.project-view-detail-list {
max-width: 640px;
width: 100%;
}
Expand Down
19 changes: 19 additions & 0 deletions web-app/packages/admin-lib/src/modules/admin/views/ProjectView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
<h2 class="headline-h2" data-cy="project-name">
<template v-if="showWorkspaceName"
><router-link
class="workspace-name-link"
v-tooltip.top="'Open workspace detail'"
:to="{
name: 'adminWorkspace',
params: { id: project?.workspace_id }
Expand All @@ -46,6 +48,12 @@
<dl
class="project-view-detail-list paragraph-p5 flex flex-column gap-3"
>
<div>
<dt class="paragraph-p6 opacity-80">Project ID</dt>
<dd class="font-semibold" data-cy="project-id">
<copy-pill :value="project?.id" label="Project ID" />
</dd>
</div>
<div>
<dt class="paragraph-p6 opacity-80">Created</dt>
<dd class="font-semibold" data-cy="project-owner">
Expand Down Expand Up @@ -118,6 +126,7 @@ import {
import { computed, watch, defineProps } from 'vue'
import { useRouter, useRoute } from 'vue-router'

import CopyPill from '@/common/components/CopyPill.vue'
import { AdminRoutes } from '@/modules'
import AdminLayout from '@/modules/admin/components/AdminLayout.vue'

Expand Down Expand Up @@ -214,8 +223,18 @@ function openDashboard() {
</script>

<style lang="scss" scoped>
h2 {
color: var(--text-color);
}

.project-view-detail-list {
max-width: 640px;
width: 100%;
}

.workspace-name-link {
text-decoration: underline dotted;
text-underline-offset: 3px;
color: inherit;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ img {
}
}

.p-divider.p-divider-horizontal {
margin: 0;
}


// Custom semantic classes

Expand Down Expand Up @@ -179,6 +183,16 @@ img {
}
}

.p-checkbox:not(.p-checkbox-disabled) {
.p-checkbox-box {
&.p-highlight:hover {
.p-checkbox-icon {
color: $primaryColor
}
}
}
}

// Color of error messages in inputs ...
.p-error {
color: map-get($map: $colors, $key: grape);
Expand Down Expand Up @@ -207,3 +221,15 @@ td:has(.dt-row-link) {
display: block;
padding: $tableBodyCellPadding;
}

// Multiselect customization of highlighting and focusing

.p-multiselect-panel .p-multiselect-items .p-multiselect-item.p-highlight.p-focus {
background-color: transparent;
color: var(--text-color);
}

.p-multiselect-panel .p-multiselect-items .p-multiselect-item.p-highlight {
background-color: transparent;
color: var(--text-color);
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ $buttonHoverBorderColor: $primaryDarkColor;

/// Background of a button in pressed state
/// @group button
$buttonActiveBg: $primaryColor;
$buttonActiveBg: $primaryDarkColor;

/// Color of a button in pressed state
/// @group button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ $checkboxActiveHoverBg: $primaryDarkerColor;

/// Icon color of a selected checkbox in hover state
/// @group form
$checkboxIconActiveHoverColor: $primaryTextColor;
$checkboxIconActiveHoverColor: $primaryColor;

/// Border color of a selected checkbox in hover state
/// @group form
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ $dividerSize:1px;

/// Color of the divider border
/// @group panel
$dividerColor:#dee2e6;
$dividerColor: map-get($colors, 'secondary-gray');

/// Background of the splitter gutter
/// @group panel
Expand Down
Loading