style: better spacing

This commit is contained in:
Shariq Ansari 2025-06-26 16:46:23 +05:30
parent cca420b1a0
commit 28ece820ed
3 changed files with 130 additions and 73 deletions

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="flex h-full flex-col gap-6 p-8 text-ink-gray-8"> <div class="flex h-full flex-col gap-6 p-6 text-ink-gray-8">
<!-- Header --> <!-- Header -->
<div class="flex justify-between"> <div class="flex justify-between px-2 pt-2">
<div class="flex flex-col gap-1 w-9/12"> <div class="flex flex-col gap-1 w-9/12">
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5"> <h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
{{ __('Email templates') }} {{ __('Email templates') }}
@ -56,7 +56,7 @@
> >
<div <div
v-if="templates.data?.length > 10" v-if="templates.data?.length > 10"
class="flex items-center justify-between mb-4" class="flex items-center justify-between mb-4 px-2 pt-0.5"
> >
<TextInput <TextInput
ref="searchRef" ref="searchRef"
@ -79,20 +79,20 @@
]" ]"
/> />
</div> </div>
<div class="flex items-center p-2 text-sm text-ink-gray-5"> <div class="flex items-center py-2 px-4 text-sm text-ink-gray-5">
<div class="w-4/6">{{ __('Template name') }}</div> <div class="w-4/6">{{ __('Template name') }}</div>
<div class="w-1/6">{{ __('For') }}</div> <div class="w-1/6">{{ __('For') }}</div>
<div class="w-1/6">{{ __('Enabled') }}</div> <div class="w-1/6">{{ __('Enabled') }}</div>
</div> </div>
<div class="h-px border-t mx-2 border-outline-gray-1" /> <div class="h-px border-t mx-4 border-outline-gray-modals" />
<ul class="overflow-y-auto"> <ul class="overflow-y-auto px-2">
<template v-for="(template, i) in templatesList" :key="template.name"> <template v-for="(template, i) in templatesList" :key="template.name">
<li <li
class="flex items-center justify-between px-2 py-3 cursor-pointer hover:bg-surface-gray-2 rounded" class="flex items-center justify-between p-3 cursor-pointer hover:bg-surface-menu-bar rounded"
@click="() => emit('updateStep', 'edit-template', { ...template })" @click="() => emit('updateStep', 'edit-template', { ...template })"
> >
<div class="flex flex-col w-4/6 pr-5"> <div class="flex flex-col w-4/6 pr-5">
<div class="text-base font-medium text-ink-gray-7"> <div class="text-base font-medium text-ink-gray-7 truncate">
{{ template.name }} {{ template.name }}
</div> </div>
<div class="text-p-base text-ink-gray-5 truncate"> <div class="text-p-base text-ink-gray-5 truncate">
@ -110,6 +110,7 @@
@click.stop @click.stop
/> />
<Dropdown <Dropdown
class=""
:options="getDropdownOptions(template)" :options="getDropdownOptions(template)"
placement="right" placement="right"
:button="{ :button="{
@ -126,7 +127,7 @@
</li> </li>
<div <div
v-if="templatesList.length !== i + 1" v-if="templatesList.length !== i + 1"
class="h-px border-t mx-2 border-outline-gray-1" class="h-px border-t mx-2 border-outline-gray-modals"
/> />
</template> </template>
<!-- Load More Button --> <!-- Load More Button -->
@ -147,6 +148,7 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { TemplateOption } from '@/utils'
import { import {
TextInput, TextInput,
FormControl, FormControl,
@ -155,7 +157,7 @@ import {
FeatherIcon, FeatherIcon,
toast, toast,
} from 'frappe-ui' } from 'frappe-ui'
import { ref, computed, inject, h } from 'vue' import { ref, computed, inject } from 'vue'
const emit = defineEmits(['updateStep']) const emit = defineEmits(['updateStep'])
@ -251,7 +253,7 @@ function getDropdownOptions(template) {
option: __('Confirm Delete'), option: __('Confirm Delete'),
icon: 'trash-2', icon: 'trash-2',
active: props.active, active: props.active,
variant: 'danger', theme: 'danger',
onClick: () => deleteTemplate(template), onClick: () => deleteTemplate(template),
}), }),
condition: () => confirmDelete.value, condition: () => confirmDelete.value,
@ -260,28 +262,4 @@ function getDropdownOptions(template) {
return options.filter((option) => option.condition?.() || true) return options.filter((option) => option.condition?.() || true)
} }
function TemplateOption({ active, option, variant, icon, onClick }) {
return h(
'button',
{
class: [
active ? 'bg-surface-gray-2' : 'text-ink-gray-8',
'group flex w-full gap-2 items-center rounded-md px-2 py-2 text-sm',
variant == 'danger' ? 'text-ink-red-3 hover:bg-ink-red-1' : '',
],
onClick: onClick,
},
[
icon
? h(FeatherIcon, {
name: icon,
class: ['h-4 w-4 shrink-0'],
'aria-hidden': true,
})
: null,
h('span', { class: 'whitespace-nowrap' }, option),
],
)
}
</script> </script>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="flex h-full flex-col gap-6 p-8 text-ink-gray-8"> <div class="flex h-full flex-col gap-6 p-6 text-ink-gray-8">
<!-- Header --> <!-- Header -->
<div class="flex justify-between"> <div class="flex justify-between px-2 pt-2">
<div class="flex flex-col gap-1 w-9/12"> <div class="flex flex-col gap-1 w-9/12">
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5"> <h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
{{ __('Users') }} {{ __('Users') }}
@ -65,7 +65,7 @@
> >
<div <div
v-if="users.data?.crmUsers?.length > 10" v-if="users.data?.crmUsers?.length > 10"
class="flex items-center justify-between mb-4" class="flex items-center justify-between mb-4 px-2 pt-0.5"
> >
<TextInput <TextInput
ref="searchRef" ref="searchRef"
@ -89,7 +89,7 @@
]" ]"
/> />
</div> </div>
<ul class="divide-y divide-outline-gray-modals overflow-y-auto"> <ul class="divide-y divide-outline-gray-modals overflow-y-auto px-2">
<template v-for="user in usersList" :key="user.name"> <template v-for="user in usersList" :key="user.name">
<li class="flex items-center justify-between py-2"> <li class="flex items-center justify-between py-2">
<div class="flex items-center"> <div class="flex items-center">
@ -112,6 +112,10 @@
:options="getMoreOptions(user)" :options="getMoreOptions(user)"
:button="{ :button="{
icon: 'more-horizontal', icon: 'more-horizontal',
onblur: (e) => {
e.stopPropagation()
confirmRemove = false
},
}" }"
placement="right" placement="right"
/> />
@ -120,6 +124,12 @@
:button="{ :button="{
label: roleMap[user.role], label: roleMap[user.role],
iconRight: 'chevron-down', iconRight: 'chevron-down',
iconLeft:
user.role === 'System Manager'
? 'shield'
: user.role === 'Sales Manager'
? 'briefcase'
: 'user-check',
}" }"
placement="right" placement="right"
/> />
@ -149,12 +159,12 @@
</template> </template>
<script setup> <script setup>
import LucideCheck from '~icons/lucide/check'
import AddExistingUserModal from '@/components/Modals/AddExistingUserModal.vue' import AddExistingUserModal from '@/components/Modals/AddExistingUserModal.vue'
import { activeSettingsPage } from '@/composables/settings' import { activeSettingsPage } from '@/composables/settings'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { Avatar, TextInput, toast, call } from 'frappe-ui' import { TemplateOption, DropdownOption } from '@/utils'
import { ref, computed, h, onMounted } from 'vue' import { Avatar, TextInput, toast, call, FeatherIcon } from 'frappe-ui'
import { ref, computed, onMounted } from 'vue'
const { users, isAdmin, isManager } = usersStore() const { users, isAdmin, isManager } = usersStore()
@ -185,12 +195,36 @@ const usersList = computed(() => {
}) })
}) })
const confirmRemove = ref(false)
function getMoreOptions(user) { function getMoreOptions(user) {
let options = [ let options = [
{ {
label: __('Remove'), label: __('Remove'),
icon: 'trash-2', component: (props) =>
onClick: () => removeUser(user, true), TemplateOption({
option: __('Remove'),
icon: 'trash-2',
active: props.active,
onClick: (e) => {
e.preventDefault()
e.stopPropagation()
confirmRemove.value = true
},
}),
condition: () => !confirmRemove.value,
},
{
label: __('Confirm Remove'),
component: (props) =>
TemplateOption({
option: __('Confirm Remove'),
icon: 'trash-2',
active: props.active,
theme: 'danger',
onClick: () => removeUser(user, true),
}),
condition: () => confirmRemove.value,
}, },
] ]
@ -202,8 +236,9 @@ function getDropdownOptions(user) {
{ {
label: __('Admin'), label: __('Admin'),
component: (props) => component: (props) =>
RoleOption({ DropdownOption({
role: __('Admin'), option: __('Admin'),
icon: 'shield',
active: props.active, active: props.active,
selected: user.role === 'System Manager', selected: user.role === 'System Manager',
onClick: () => updateRole(user, 'System Manager'), onClick: () => updateRole(user, 'System Manager'),
@ -213,8 +248,9 @@ function getDropdownOptions(user) {
{ {
label: __('Manager'), label: __('Manager'),
component: (props) => component: (props) =>
RoleOption({ DropdownOption({
role: __('Manager'), option: __('Manager'),
icon: 'briefcase',
active: props.active, active: props.active,
selected: user.role === 'Sales Manager', selected: user.role === 'Sales Manager',
onClick: () => updateRole(user, 'Sales Manager'), onClick: () => updateRole(user, 'Sales Manager'),
@ -224,8 +260,9 @@ function getDropdownOptions(user) {
{ {
label: __('Sales User'), label: __('Sales User'),
component: (props) => component: (props) =>
RoleOption({ DropdownOption({
role: __('Sales User'), option: __('Sales User'),
icon: 'user-check',
active: props.active, active: props.active,
selected: user.role === 'Sales User', selected: user.role === 'Sales User',
onClick: () => updateRole(user, 'Sales User'), onClick: () => updateRole(user, 'Sales User'),
@ -236,28 +273,6 @@ function getDropdownOptions(user) {
return options.filter((option) => option.condition?.() || true) return options.filter((option) => option.condition?.() || true)
} }
function RoleOption({ active, role, onClick, selected }) {
return h(
'button',
{
class: [
active ? 'bg-surface-gray-2' : 'text-ink-gray-8',
'group flex w-full justify-between items-center rounded-md px-2 py-2 text-sm',
],
onClick: !selected ? onClick : null,
},
[
h('span', { class: 'whitespace-nowrap' }, role),
selected
? h(LucideCheck, {
class: ['h-4 w-4 shrink-0 text-ink-gray-7'],
'aria-hidden': true,
})
: null,
],
)
}
function updateRole(user, newRole) { function updateRole(user, newRole) {
if (user.role === newRole) return if (user.role === newRole) return

View File

@ -1,9 +1,10 @@
import LucideCheck from '~icons/lucide/check'
import TaskStatusIcon from '@/components/Icons/TaskStatusIcon.vue' import TaskStatusIcon from '@/components/Icons/TaskStatusIcon.vue'
import TaskPriorityIcon from '@/components/Icons/TaskPriorityIcon.vue' import TaskPriorityIcon from '@/components/Icons/TaskPriorityIcon.vue'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { gemoji } from 'gemoji' import { gemoji } from 'gemoji'
import { getMeta } from '@/stores/meta' import { getMeta } from '@/stores/meta'
import { toast, dayjsLocal, dayjs, getConfig } from 'frappe-ui' import { toast, dayjsLocal, dayjs, getConfig, FeatherIcon } from 'frappe-ui'
import { h } from 'vue' import { h } from 'vue'
export function formatTime(seconds) { export function formatTime(seconds) {
@ -465,3 +466,66 @@ export function runSequentially(functions) {
return promise.then(() => fn()) return promise.then(() => fn())
}, Promise.resolve()) }, Promise.resolve())
} }
export function DropdownOption({
active,
option,
theme,
icon,
onClick,
selected,
}) {
return h(
'button',
{
class: [
active ? 'bg-surface-gray-2' : 'text-ink-gray-8',
'group flex w-full justify-between items-center rounded-md px-2 py-2 text-sm',
theme == 'danger' ? 'text-ink-red-3 hover:bg-ink-red-1' : '',
],
onClick: !selected ? onClick : null,
},
[
h('div', { class: 'flex gap-2' }, [
icon
? h(FeatherIcon, {
name: icon,
class: ['h-4 w-4 shrink-0'],
'aria-hidden': true,
})
: null,
h('span', { class: 'whitespace-nowrap' }, option),
]),
selected
? h(LucideCheck, {
class: ['h-4 w-4 shrink-0 text-ink-gray-7'],
'aria-hidden': true,
})
: null,
],
)
}
export function TemplateOption({ active, option, theme, icon, onClick }) {
return h(
'button',
{
class: [
active ? 'bg-surface-gray-2 text-ink-gray-8' : 'text-ink-gray-7',
'group flex w-full gap-2 items-center rounded-md px-2 py-2 text-sm',
theme == 'danger' ? 'text-ink-red-3 hover:bg-ink-red-1' : '',
],
onClick: onClick,
},
[
icon
? h(FeatherIcon, {
name: icon,
class: ['h-4 w-4 shrink-0'],
'aria-hidden': true,
})
: null,
h('span', { class: 'whitespace-nowrap' }, option),
],
)
}