Skip to content

Commit

Permalink
Merge pull request #68 from krantheman/feat-admin-dashboard
Browse files Browse the repository at this point in the history
feat: Admin Dashboard
  • Loading branch information
s-aga-r authored Feb 7, 2025
2 parents d2e279a + d0a39e4 commit 37068a7
Show file tree
Hide file tree
Showing 39 changed files with 2,767 additions and 938 deletions.
1 change: 1 addition & 0 deletions frappe-ui
Submodule frappe-ui added at 1c9749
10 changes: 4 additions & 6 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,14 @@
"scripts": {
"dev": "vite",
"serve": "vite preview",
"build": "yarn build-app && yarn build-email-css",
"build-app": "vite build --base=/assets/mail/frontend/ && yarn copy-html-entry",
"copy-html-entry": "cp ../mail/public/frontend/index.html ../mail/www/mail.html",
"build-email-css": "npx tailwindcss -i ../mail/public/email/style.css -o ../mail/public/css/email.css --config ../mail/public/email/tailwind.config.js"
"build": "vite build --base=/assets/mail/frontend/ && yarn copy-html-entry",
"copy-html-entry": "cp ../mail/public/frontend/index.html ../mail/www/mail.html"
},
"dependencies": {
"@vueuse/core": "^10.4.1",
"dayjs": "^1.11.11",
"feather-icons": "^4.28.0",
"frappe-ui": "^0.1.60",
"frappe-ui": "^0.1.108",
"gemoji": "^8.1.0",
"lucide-vue-next": "^0.383.0",
"markdown-it": "^14.1.0",
Expand All @@ -27,6 +25,6 @@
"autoprefixer": "^10.4.2",
"postcss": "^8.4.5",
"tailwindcss": "^3.0.15",
"vite": "^6.0.11"
"vite": "^5"
}
}
11 changes: 9 additions & 2 deletions frontend/src/components/AppSidebar.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div
class="flex h-full flex-col justify-between transition-all duration-300 ease-in-out bg-gray-100"
class="flex h-full flex-col justify-between transition-all duration-300 ease-in-out bg-gray-50 border-r"
:class="isSidebarCollapsed ? 'w-14' : 'w-56'"
>
<div
Expand Down Expand Up @@ -40,14 +40,21 @@
</template>

<script setup>
import { useRoute } from 'vue-router'
import UserDropdown from '@/components/UserDropdown.vue'
import SidebarLink from '@/components/SidebarLink.vue'
import { useStorage } from '@vueuse/core'
import { ref, computed } from 'vue'
import { ArrowLeftFromLine } from 'lucide-vue-next'
import { getSidebarLinks } from '../utils'
const sidebarLinks = computed(() => getSidebarLinks())
const route = useRoute()
const sidebarLinks = computed(() =>
getSidebarLinks().filter((link) =>
route.meta.isDashboard ? link.forDashboard : !link.forDashboard
)
)
const getSidebarFromStorage = () => {
return useStorage('sidebar_is_collapsed', false)
Expand Down
33 changes: 25 additions & 8 deletions frontend/src/components/Controls/Copy.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
<template>
<label class="block pt-2 text-sm">
<div class="block pt-2 text-sm">
<span class="mb-2 block leading-4 text-gray-700">{{ props.label }}</span>
<button
class="border-2 rounded-lg bg-gray-100 p-2 w-full flex items-center"
@click="copyToClipBoard(props.value)"
>
<span class="text-gray-800">{{ props.value }}</span>
<span class="text-gray-800 text-nowrap overflow-x-scroll mr-1.5 scrollbar-none">
{{ props.value }}
</span>
<span class="border rounded bg-white p-1 text-gray-600 text-xs ml-auto">
{{ message }}
</span>
</button>
</label>
</div>
</template>
<script setup>
import { ref } from 'vue'
Expand All @@ -29,10 +31,25 @@ const props = defineProps({
})
const copyToClipBoard = async (text) => {
await navigator.clipboard.writeText(text)
message.value = 'Copied!'
setTimeout(() => {
message.value = 'Copy'
}, 2000)
try {
await navigator.clipboard.writeText(text)
message.value = 'Copied!'
setTimeout(() => {
message.value = 'Copy'
}, 2000)
} catch (e) {
alert('Failed to copy text. Please copy from here: ' + text)
}
}
</script>

<style>
.scrollbar-none::-webkit-scrollbar {
display: none;
}
.scrollbar-none {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
21 changes: 21 additions & 0 deletions frontend/src/components/Controls/HorizontalFormControl.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<template>
<div class="grid grid-cols-1 sm:grid-cols-2 px-2.5" :class="{ 'text-ink-gray-4': disabled }">
<label class="font-medium leading-normal my-auto">
{{ label }}
</label>
<slot />
</div>
</template>

<script setup>
const props = defineProps({
label: {
type: String,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
})
</script>
95 changes: 95 additions & 0 deletions frontend/src/components/Modals/AddDomain.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<template>
<Dialog
v-model="show"
:options="{
title: __('Add Domain'),
actions: [
{
label: __(domainRequest?.data ? 'Verify DNS' : 'Add Domain'),
variant: 'solid',
onClick: domainRequest?.data ? verifyDNS.submit : domainRequest.submit,
},
],
}"
>
<template #body-content>
<div class="space-y-4">
<p class="text-p-base">
{{
__(
`To add a domain, you must already own it. If you don't have one, purchase one and return here.`
)
}}
</p>
<FormControl
type="text"
:label="__('Domain Name')"
placeholder="example.com"
v-model="domainName"
:readonly="!!domainRequest?.data"
autocomplete="off"
/>
<ErrorMessage :message="domainRequest.error?.messages[0]" />
<div v-if="domainRequest.data?.verification_key" class="space-y-4">
<p class="text-p-base">
{{
__(
`Add the following TXT record to your domain's DNS records to verify your ownership:`
)
}}
</p>
<Copy
:label="__('Verification Key')"
:value="domainRequest.data.verification_key"
/>
<ErrorMessage :message="verifyDNS.error?.messages[0] || verificationError" />
</div>
</div>
</template>
</Dialog>
</template>

<script setup>
import { ref, inject, watch } from 'vue'
import { Dialog, FormControl, ErrorMessage, createResource } from 'frappe-ui'
import Copy from '@/components/Controls/Copy.vue'
import { raiseToast } from '@/utils'
const show = defineModel()
const user = inject('$user')
const domainName = ref('')
const verificationError = ref('')
const emit = defineEmits(['reloadDomains'])
watch(show, () => {
if (show.value) {
domainName.value = ''
verificationError.value = ''
domainRequest.reset()
verifyDNS.reset()
}
})
const domainRequest = createResource({
url: 'mail.api.admin.get_domain_request',
makeParams() {
return { domain_name: domainName.value, mail_tenant: user.data.tenant }
},
})
const verifyDNS = createResource({
url: 'mail.api.admin.verify_dns_record',
makeParams() {
return { domain_request: domainRequest?.data.name }
},
onSuccess(data) {
if (data) {
show.value = false
emit('reloadDomains')
raiseToast('Domain added successfully!')
} else verificationError.value = __('Failed to verify DNS record.')
},
})
</script>
142 changes: 142 additions & 0 deletions frontend/src/components/Modals/AddMember.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<template>
<Dialog
v-model="show"
:options="{
title: __('Add Member'),
actions: [
{
label: __(accountRequest.send_invite ? 'Invite Member' : 'Add Member'),
variant: 'solid',
onClick: addMember.submit,
},
],
}"
>
<template #body-content>
<div class="space-y-4">
<div class="flex items-center justify-between">
<FormControl
type="text"
:label="__('Username')"
placeholder="johndoe"
v-model="accountRequest.username"
class="w-full"
/>
<FeatherIcon
class="text-gray-400 h-4 w-4 mt-auto mb-1.5 mx-2.5"
name="at-sign"
/>
<Link
:label="__('Domain')"
placeholder="yourdomain.com"
v-model="accountRequest.domain"
doctype="Mail Domain"
:filters="{ tenant: user.data.tenant }"
class="w-full"
/>
</div>
<FormControl
type="select"
:label="__('Member Role')"
:options="[
{ label: __('Mail User'), value: 'Mail User' },
{ label: __('Mail Admin'), value: 'Mail Admin' },
]"
v-model="accountRequest.role"
/>
<hr />
<FormControl
type="checkbox"
:label="__('Send Invite')"
v-model="accountRequest.send_invite"
/>
<FormControl
v-if="accountRequest.send_invite"
type="email"
:label="__('Email')"
placeholder="[email protected]"
v-model="accountRequest.email"
/>
<template v-else>
<FormControl
type="text"
:label="__('First Name')"
placeholder="John"
v-model="accountRequest.first_name"
/>
<FormControl
type="text"
:label="__('Last Name')"
placeholder="Doe"
v-model="accountRequest.last_name"
/>
<FormControl
type="password"
:label="__('Password')"
placeholder="••••••••"
v-model="accountRequest.password"
/>
</template>
<ErrorMessage :message="addMember.error?.messages[0]" />
</div>
</template>
</Dialog>
</template>

<script setup>
import { reactive, inject, watch } from 'vue'
import { Dialog, FeatherIcon, FormControl, ErrorMessage, createResource } from 'frappe-ui'
import Link from '@/components/Controls/Link.vue'
import { raiseToast } from '@/utils'
const show = defineModel()
const user = inject('$user')
const defaultAccountRequest = {
username: '',
domain: '',
role: 'Mail User',
send_invite: true,
email: '',
first_name: '',
last_name: '',
password: '',
}
const accountRequest = reactive({ ...defaultAccountRequest })
const emit = defineEmits(['reloadMembers'])
watch(
() => accountRequest.send_invite,
() => addMember.reset()
)
watch(show, () => {
if (show.value) {
Object.assign(accountRequest, defaultAccountRequest)
addMember.reset()
}
})
const addMember = createResource({
url: 'mail.api.admin.add_member',
makeParams() {
return {
tenant: user.data.tenant,
...accountRequest,
}
},
onSuccess() {
raiseToast(
__(
accountRequest.send_invite
? 'Member invited successfully'
: 'Member added successfully'
)
)
if (!accountRequest.send_invite) emit('reloadMembers')
show.value = false
},
})
</script>
1 change: 1 addition & 0 deletions frontend/src/components/Modals/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { Dialog, Button } from 'frappe-ui'
import { User, Mailbox } from 'lucide-vue-next'
const show = defineModel()
const tabs = [
{
label: 'User',
Expand Down
Loading

0 comments on commit 37068a7

Please sign in to comment.