Merge pull request #496 from shariquerik/crm-settings

This commit is contained in:
Shariq Ansari 2024-12-30 18:48:42 +05:30 committed by GitHub
commit c0732060e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 394 additions and 162 deletions

View File

@ -46,16 +46,13 @@ def get_fields_layout(doctype: str, type: str):
for field in section.get("fields") if section.get("fields") else []: for field in section.get("fields") if section.get("fields") else []:
field = next((f for f in fields if f.fieldname == field), None) field = next((f for f in fields if f.fieldname == field), None)
if field: if field:
if field.fieldtype == "Select" and field.options:
field.options = field.options.split("\n")
field.options = [{"label": _(option), "value": option} for option in field.options]
field.options.insert(0, {"label": "", "value": ""})
field = { field = {
"label": _(field.label), "label": _(field.label),
"name": field.fieldname, "name": field.fieldname,
"type": field.fieldtype, "type": field.fieldtype,
"options": field.options, "options": getOptions(field),
"mandatory": field.reqd, "mandatory": field.reqd,
"read_only": field.read_only,
"placeholder": field.get("placeholder"), "placeholder": field.get("placeholder"),
"filters": field.get("link_filters"), "filters": field.get("link_filters"),
} }
@ -86,17 +83,17 @@ def save_fields_layout(doctype: str, type: str, layout: str):
def get_default_layout(doctype: str): def get_default_layout(doctype: str):
fields = frappe.get_meta(doctype).fields fields = frappe.get_meta(doctype).fields
fields = [ fields = [
{ field.fieldname
"label": _(field.label),
"name": field.fieldname,
"type": field.fieldtype,
"options": field.options,
"mandatory": field.reqd,
"placeholder": field.get("placeholder"),
"filters": field.get("link_filters"),
}
for field in fields for field in fields
if field.fieldtype not in ["Section Break", "Column Break"] if field.fieldtype not in ["Tab Break", "Section Break", "Column Break"]
] ]
return [{"no_tabs": True, "sections": [{"hideLabel": True, "fields": fields}]}] return [{"no_tabs": True, "sections": [{"hideLabel": True, "fields": fields}]}]
def getOptions(field):
if field.fieldtype == "Select" and field.options:
field.options = field.options.split("\n")
field.options = [{"label": _(option), "value": option} for option in field.options]
field.options.insert(0, {"label": "", "value": ""})
return field.options

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="flex flex-col text-base"> <div class="flex flex-col flex-1 text-base">
<div v-if="label" class="mb-1.5 text-sm text-ink-gray-5"> <div v-if="label" class="mb-1.5 text-sm text-ink-gray-5">
{{ __(label) }} {{ __(label) }}
</div> </div>
@ -55,9 +55,16 @@
<template #item="{ element: row, index }"> <template #item="{ element: row, index }">
<div <div
class="grid-row flex cursor-pointer items-center border-b border-outline-gray-modals bg-surface-modals last:rounded-b last:border-b-0" class="grid-row flex cursor-pointer items-center border-b border-outline-gray-modals bg-surface-modals last:rounded-b last:border-b-0"
@click.stop="
() => {
if (!gridSettings.editable_grid) {
showRowList[index] = true
}
}
"
> >
<div <div
class="inline-flex h-9.5 items-center justify-center border-r border-outline-gray-modals p-2 w-12" class="grid-row-checkbox inline-flex h-9.5 items-center bg-surface-white justify-center border-r border-outline-gray-modals p-2 w-12"
> >
<Checkbox <Checkbox
class="cursor-pointer duration-300" class="cursor-pointer duration-300"
@ -66,7 +73,7 @@
/> />
</div> </div>
<div <div
class="flex h-9.5 items-center justify-center border-r border-outline-gray-modals py-2 px-1 text-sm text-ink-gray-8 w-12" class="flex h-9.5 items-center justify-center bg-surface-white border-r border-outline-gray-modals py-2 px-1 text-sm text-ink-gray-8 w-12"
> >
{{ index + 1 }} {{ index + 1 }}
</div> </div>
@ -88,11 +95,12 @@
/> />
<div <div
v-else-if="field.type === 'Check'" v-else-if="field.type === 'Check'"
class="flex h-full justify-center items-center" class="flex h-full bg-surface-white justify-center items-center"
> >
<Checkbox <Checkbox
class="cursor-pointer duration-300" class="cursor-pointer duration-300"
v-model="row[field.name]" v-model="row[field.name]"
:disabled="!gridSettings.editable_grid"
/> />
</div> </div>
<DatePicker <DatePicker
@ -233,7 +241,10 @@ const props = defineProps({
}, },
}) })
const { getGridSettings, getFields } = getMeta(props.doctype) const { getGridViewSettings, getFields, getGridSettings } = getMeta(
props.doctype,
)
getMeta(props.parentDoctype)
const rows = defineModel() const rows = defineModel()
const showRowList = ref(new Array(rows.value?.length || []).fill(false)) const showRowList = ref(new Array(rows.value?.length || []).fill(false))
@ -242,11 +253,13 @@ const selectedRows = reactive(new Set())
const showGridFieldsEditorModal = ref(false) const showGridFieldsEditorModal = ref(false)
const showGridRowFieldsModal = ref(false) const showGridRowFieldsModal = ref(false)
const gridSettings = computed(() => getGridSettings())
const fields = computed(() => { const fields = computed(() => {
let gridSettings = getGridSettings(props.parentDoctype) let gridViewSettings = getGridViewSettings(props.parentDoctype)
let gridFields = getFields() let gridFields = getFields()
if (gridSettings.length) { if (gridViewSettings.length) {
let d = gridSettings.map((gs) => let d = gridViewSettings.map((gs) =>
getFieldObj(gridFields.find((f) => f.fieldname === gs.fieldname)), getFieldObj(gridFields.find((f) => f.fieldname === gs.fieldname)),
) )
return d return d
@ -269,9 +282,11 @@ function getFieldObj(field) {
const gridTemplateColumns = computed(() => { const gridTemplateColumns = computed(() => {
if (!fields.value?.length) return '1fr' if (!fields.value?.length) return '1fr'
// for the checkbox & sr no. columns // for the checkbox & sr no. columns
let gridSettings = getGridSettings(props.parentDoctype) let gridViewSettings = getGridViewSettings(props.parentDoctype)
if (gridSettings.length) { if (gridViewSettings.length) {
return gridSettings.map((gs) => `minmax(0, ${gs.columns || 2}fr)`).join(' ') return gridViewSettings
.map((gs) => `minmax(0, ${gs.columns || 2}fr)`)
.join(' ')
} }
return fields.value.map(() => `minmax(0, 2fr)`).join(' ') return fields.value.map(() => `minmax(0, 2fr)`).join(' ')
}) })
@ -353,6 +368,10 @@ const deleteRows = () => {
height: 38px; height: 38px;
} }
:deep(.grid-row:last-child .grid-row-checkbox) {
border-bottom-left-radius: 7px;
}
:deep(.grid-row .edit-row button) { :deep(.grid-row .edit-row button) {
border-bottom-right-radius: 7px; border-bottom-right-radius: 7px;
} }

View File

@ -107,7 +107,7 @@ const props = defineProps({
parentDoctype: String, parentDoctype: String,
}) })
const { userSettings, getFields, getGridSettings, saveUserSettings } = getMeta( const { getFields, getGridViewSettings, saveUserSettings } = getMeta(
props.doctype, props.doctype,
) )
@ -122,10 +122,10 @@ const dirty = computed(() => {
const oldFields = computed(() => { const oldFields = computed(() => {
let _fields = getFields() let _fields = getFields()
let gridSettings = getGridSettings(props.parentDoctype) let gridViewSettings = getGridViewSettings(props.parentDoctype)
if (gridSettings.length) { if (gridViewSettings.length) {
return gridSettings.map((field) => { return gridViewSettings.map((field) => {
let f = _fields.find((f) => f.fieldname === field.fieldname) let f = _fields.find((f) => f.fieldname === field.fieldname)
if (f) { if (f) {
f.columns = field.columns f.columns = field.columns
@ -175,14 +175,6 @@ function update() {
saveUserSettings(props.parentDoctype, 'GridView', updateFields, () => { saveUserSettings(props.parentDoctype, 'GridView', updateFields, () => {
loading.value = false loading.value = false
show.value = false show.value = false
if (userSettings[props.parentDoctype]?.['GridView']) {
userSettings[props.parentDoctype]['GridView'][props.doctype] =
updateFields
} else {
userSettings[props.parentDoctype] = {
GridView: { [props.doctype]: updateFields },
}
}
}) })
} }

View File

@ -33,7 +33,6 @@
<script setup> <script setup>
import EditIcon from '@/components/Icons/EditIcon.vue' import EditIcon from '@/components/Icons/EditIcon.vue'
import FieldLayout from '@/components/FieldLayout.vue' import FieldLayout from '@/components/FieldLayout.vue'
import { getMeta } from '@/stores/meta'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { createResource } from 'frappe-ui' import { createResource } from 'frappe-ui'
import { nextTick } from 'vue' import { nextTick } from 'vue'
@ -44,7 +43,6 @@ const props = defineProps({
doctype: String, doctype: String,
}) })
const { getFields } = getMeta(props.doctype)
const { isManager } = usersStore() const { isManager } = usersStore()
const show = defineModel() const show = defineModel()
@ -52,28 +50,9 @@ const showGridRowFieldsModal = defineModel('showGridRowFieldsModal')
const tabs = createResource({ const tabs = createResource({
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout', url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
cache: ['GridRow', props.doctype], cache: ['Grid Row', props.doctype],
params: { doctype: props.doctype, type: 'Grid Row' }, params: { doctype: props.doctype, type: 'Grid Row' },
auto: true, auto: true,
transform: (data) => {
if (data.length) return data
let fields = getFields()
if (!fields) return []
return [
{
no_tabs: true,
sections: [
{
hideLabel: true,
opened: true,
fields: fields.map((f) => {
return { ...f, name: f.fieldname, type: f.fieldtype }
}),
},
],
},
]
},
}) })
function openGridRowFieldsModal() { function openGridRowFieldsModal() {

View File

@ -0,0 +1,42 @@
<template>
<FileUploader
:file-types="image_type"
class="text-base"
@success="
(file) => {
$emit('upload', file.file_url)
}
"
>
<template v-slot="{ progress, uploading, openFileSelector }">
<div class="flex items-end space-x-1">
<Button @click="openFileSelector">
{{
uploading
? `Uploading ${progress}%`
: image_url
? 'Change'
: 'Upload'
}}
</Button>
<Button v-if="image_url" @click="$emit('remove')">Remove</Button>
</div>
</template>
</FileUploader>
</template>
<script setup>
import { FileUploader, Button } from 'frappe-ui'
const prop = defineProps({
image_url: String,
image_type: {
type: String,
default: 'image/*',
},
label: {
type: String,
default: '',
},
})
const emit = defineEmits(['upload', 'remove'])
</script>

View File

@ -9,7 +9,7 @@
<Tabs <Tabs
v-model="tabIndex" v-model="tabIndex"
class="!h-full" class="!h-full"
:tabs="tabs" :tabs="_tabs"
v-slot="{ tab }" v-slot="{ tab }"
:tablistClass=" :tablistClass="
!hasTabs ? 'hidden' : modal ? 'border-outline-gray-modals' : '' !hasTabs ? 'hidden' : modal ? 'border-outline-gray-modals' : ''
@ -44,16 +44,7 @@
]" ]"
> >
<div v-for="field in section.fields" :key="field.name"> <div v-for="field in section.fields" :key="field.name">
<div <div class="settings-field">
class="settings-field"
v-if="
(field.type == 'Check' ||
(field.read_only && data[field.name]) ||
!field.read_only ||
!field.hidden) &&
(!field.depends_on || field.display_via_depends_on)
"
>
<div <div
v-if="field.type != 'Check'" v-if="field.type != 'Check'"
class="mb-2 text-sm text-ink-gray-5" class="mb-2 text-sm text-ink-gray-5"
@ -79,7 +70,6 @@
<Grid <Grid
v-else-if="field.type === 'Table'" v-else-if="field.type === 'Table'"
v-model="data[field.name]" v-model="data[field.name]"
:fields="field.fields"
:doctype="field.options" :doctype="field.options"
:parentDoctype="doctype" :parentDoctype="doctype"
/> />
@ -109,7 +99,13 @@
/> />
<label <label
class="text-sm text-ink-gray-5" class="text-sm text-ink-gray-5"
@click="data[field.name] = !data[field.name]" @click="
() => {
if (!Boolean(field.read_only)) {
data[field.name] = !data[field.name]
}
}
"
> >
{{ __(field.label) }} {{ __(field.label) }}
<span class="text-ink-red-3" v-if="field.mandatory" <span class="text-ink-red-3" v-if="field.mandatory"
@ -273,6 +269,23 @@ const { getUser } = usersStore()
const hasTabs = computed(() => !props.tabs[0].no_tabs) const hasTabs = computed(() => !props.tabs[0].no_tabs)
const _tabs = computed(() => {
return props.tabs.map((tab) => {
tab.sections = tab.sections.map((section) => {
section.fields = section.fields.filter(
(field) =>
(field.type == 'Check' ||
(field.read_only && props.data[field.name]) ||
!field.read_only) &&
(!field.depends_on || field.display_via_depends_on) &&
!field.hidden,
)
return section
})
return tab
})
})
const tabIndex = ref(0) const tabIndex = ref(0)
function gridClass(columns) { function gridClass(columns) {

View File

@ -0,0 +1,133 @@
<template>
<div class="flex h-full flex-col gap-8 p-8 text-ink-gray-9">
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
{{ __('General') }}
<Badge
v-if="settings.isDirty"
:label="__('Not Saved')"
variant="subtle"
theme="orange"
/>
</h2>
<div v-if="settings.doc" class="flex-1 flex flex-col gap-8 overflow-y-auto">
<div class="flex w-full">
<FormControl
type="text"
class="w-1/2"
v-model="settings.doc.brand_name"
:label="__('Brand Name')"
/>
</div>
<!-- logo -->
<div class="flex flex-col justify-between gap-4">
<span class="text-base font-semibold text-ink-gray-9">
{{ __('Logo') }}
</span>
<div class="flex flex-1 gap-5">
<div
class="flex items-center justify-center rounded border border-outline-gray-modals px-10 py-2"
>
<img
:src="settings.doc?.brand_logo || '/assets/crm/images/logo.png'"
alt="Logo"
class="size-8 rounded"
/>
</div>
<div class="flex flex-1 flex-col gap-2">
<ImageUploader
label="Favicon"
image_type="image/ico"
:image_url="settings.doc?.brand_logo"
@upload="(url) => (settings.doc.brand_logo = url)"
@remove="() => (settings.doc.brand_logo = '')"
/>
<span class="text-p-sm text-ink-gray-6">
{{
__(
'Appears in the top sidebar. Recommended size is 32x32 px in PNG or SVG',
)
}}
</span>
</div>
</div>
</div>
<!-- favicon -->
<div class="flex flex-col justify-between gap-4">
<span class="text-base font-semibold text-ink-gray-9">
{{ __('Favicon') }}
</span>
<div class="flex flex-1 gap-5">
<div
class="flex items-center justify-center rounded border border-outline-gray-modals px-10 py-2"
>
<img
:src="settings.doc?.favicon || '/assets/crm/images/logo.png'"
alt="Favicon"
class="size-8 rounded"
/>
</div>
<div class="flex flex-1 flex-col gap-2">
<ImageUploader
label="Favicon"
image_type="image/ico"
:image_url="settings.doc?.favicon"
@upload="(url) => (settings.doc.favicon = url)"
@remove="() => (settings.doc.favicon = '')"
/>
<span class="text-p-sm text-ink-gray-6">
{{
__(
'Appears next to the title in your browser tab. Recommended size is 32x32 px in PNG or ICO',
)
}}
</span>
</div>
</div>
</div>
<!-- dropdown settings -->
<div class="flex flex-col justify-between gap-4">
<span class="text-base font-semibold text-ink-gray-9">
{{ __('Dropdown settings') }}
</span>
<div class="flex flex-1">
<Grid
v-model="settings.doc.dropdown_items"
doctype="CRM Dropdown Item"
parentDoctype="FCRM Settings"
/>
</div>
</div>
</div>
<div class="flex flex-row-reverse">
<Button
variant="solid"
:label="__('Update')"
:disabled="!settings.isDirty"
@click="updateSettings"
/>
</div>
</div>
</template>
<script setup>
import ImageUploader from '@/components/Controls/ImageUploader.vue'
import Grid from '@/components/Controls/Grid.vue'
import { FormControl, Badge } from 'frappe-ui'
import { getSettings } from '@/stores/settings'
import { showSettings } from '@/composables/settings'
const { _settings: settings, setupBrand } = getSettings()
function updateSettings() {
settings.save.submit()
showSettings.value = false
setupBrand()
}
</script>

View File

@ -3,62 +3,68 @@
<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">
{{ __('Send Invites To') }} {{ __('Send Invites To') }}
</h2> </h2>
<div class="flex-1 overflow-y-auto"> <div class="flex-1 flex flex-col gap-8 overflow-y-auto">
<label class="block text-xs text-ink-gray-5 mb-1.5"> <div>
{{ __('Invite by email') }} <label class="block text-xs text-ink-gray-5 mb-1.5">
</label> {{ __('Invite by email') }}
<MultiValueInput </label>
v-model="invitees" <MultiValueInput
:validate="validateEmail" v-model="invitees"
:error-message=" :validate="validateEmail"
(value) => __('{0} is an invalid email address', [value]) :error-message="
" (value) => __('{0} is an invalid email address', [value])
/> "
<FormControl />
type="select" <FormControl
class="mt-4" type="select"
v-model="role" class="mt-4"
:label="__('Invite as')" v-model="role"
:options="[ :label="__('Invite as')"
{ label: __('Regular Access'), value: 'Sales User' }, :options="[
{ label: __('Manager Access'), value: 'Sales Manager' }, { label: __('Regular Access'), value: 'Sales User' },
]" { label: __('Manager Access'), value: 'Sales Manager' },
:description="description" ]"
/> :description="description"
<ErrorMessage class="mt-2" v-if="error" :message="error" /> />
<ErrorMessage class="mt-2" v-if="error" :message="error" />
</div>
<template v-if="pendingInvitations.data?.length && !invitees.length"> <template v-if="pendingInvitations.data?.length && !invitees.length">
<div <div class="flex flex-col gap-4">
class="mt-6 flex items-center justify-between py-4 text-base font-semibold" <div
> class="flex items-center justify-between text-base font-semibold"
<div>{{ __('Pending Invites') }}</div>
</div>
<ul class="flex flex-col gap-1">
<li
class="flex items-center justify-between px-2 py-1 rounded-lg bg-surface-gray-2"
v-for="user in pendingInvitations.data"
:key="user.name"
> >
<div class="text-base"> <div>{{ __('Pending Invites') }}</div>
<span class="text-ink-gray-9"> </div>
{{ user.email }} <ul class="flex flex-col gap-1">
</span> <li
<span class="text-ink-gray-5"> ({{ roleMap[user.role] }}) </span> class="flex items-center justify-between px-2 py-1 rounded-lg bg-surface-gray-2"
</div> v-for="user in pendingInvitations.data"
<div> :key="user.name"
<Tooltip text="Delete Invitation"> >
<Button <div class="text-base">
icon="x" <span class="text-ink-gray-9">
variant="ghost" {{ user.email }}
:loading=" </span>
pendingInvitations.delete.loading && <span class="text-ink-gray-5">
pendingInvitations.delete.params.name === user.name ({{ roleMap[user.role] }})
" </span>
@click="pendingInvitations.delete.submit(user.name)" </div>
/> <div>
</Tooltip> <Tooltip text="Delete Invitation">
</div> <Button
</li> icon="x"
</ul> variant="ghost"
:loading="
pendingInvitations.delete.loading &&
pendingInvitations.delete.params.name === user.name
"
@click="pendingInvitations.delete.submit(user.name)"
/>
</Tooltip>
</div>
</li>
</ul>
</div>
</template> </template>
</div> </div>
<div class="flex flex-row-reverse"> <div class="flex flex-row-reverse">

View File

@ -33,7 +33,14 @@
</nav> </nav>
</div> </div>
</div> </div>
<div class="flex flex-1 flex-col overflow-y-auto bg-surface-modal"> <div
class="flex relative flex-1 flex-col overflow-y-auto bg-surface-modal"
>
<Button
class="absolute right-5 top-5"
icon="x"
@click="showSettings = false"
/>
<component :is="activeTab.component" v-if="activeTab" /> <component :is="activeTab.component" v-if="activeTab" />
</div> </div>
</div> </div>
@ -44,6 +51,7 @@
import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue' import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
import ERPNextIcon from '@/components/Icons/ERPNextIcon.vue' import ERPNextIcon from '@/components/Icons/ERPNextIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue' import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import GeneralSettings from '@/components/Settings/GeneralSettings.vue'
import InviteMemberPage from '@/components/Settings/InviteMemberPage.vue' import InviteMemberPage from '@/components/Settings/InviteMemberPage.vue'
import ProfileSettings from '@/components/Settings/ProfileSettings.vue' import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue' import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue'
@ -56,7 +64,7 @@ import {
showSettings, showSettings,
activeSettingsPage, activeSettingsPage,
} from '@/composables/settings' } from '@/composables/settings'
import { Dialog, Avatar } from 'frappe-ui' import { Dialog, Button, Avatar } from 'frappe-ui'
import { ref, markRaw, computed, watch, h } from 'vue' import { ref, markRaw, computed, watch, h } from 'vue'
const { isManager, getUser } = usersStore() const { isManager, getUser } = usersStore()
@ -79,6 +87,12 @@ const tabs = computed(() => {
}), }),
component: markRaw(ProfileSettings), component: markRaw(ProfileSettings),
}, },
{
label: __('General'),
icon: 'settings',
component: markRaw(GeneralSettings),
condition: () => isManager(),
},
{ {
label: __('Invite Members'), label: __('Invite Members'),
icon: 'user-plus', icon: 'user-plus',

View File

@ -76,7 +76,6 @@ const data = createDocumentResource({
doctype: props.doctype, doctype: props.doctype,
name: props.doctype, name: props.doctype,
fields: ['*'], fields: ['*'],
cache: props.doctype,
auto: true, auto: true,
setValue: { setValue: {
onSuccess: () => { onSuccess: () => {
@ -101,19 +100,38 @@ const data = createDocumentResource({
const tabs = computed(() => { const tabs = computed(() => {
if (!fields.data) return [] if (!fields.data) return []
let _sections = [] let _tabs = []
let fieldsData = fields.data let fieldsData = fields.data
if (fieldsData[0].type !== 'Section Break') { if (fieldsData[0].type != 'Tab Break') {
_sections.push({ let _sections = []
label: 'General', if (fieldsData[0].type != 'Section Break') {
hideLabel: true, _sections.push({
columns: 1, hideLabel: true,
fields: [], columns: 1,
fields: [],
})
}
_tabs.push({
no_tabs: true,
sections: _sections,
}) })
} }
fieldsData.forEach((field) => { fieldsData.forEach((field) => {
if (field.type === 'Section Break') { let _sections = _tabs.length ? _tabs[_tabs.length - 1].sections : []
if (field.type === 'Tab Break') {
_tabs.push({
label: field.label,
sections: [
{
hideLabel: true,
columns: 1,
fields: [],
},
],
})
} else if (field.type === 'Section Break') {
_sections.push({ _sections.push({
label: field.value, label: field.value,
hideLabel: true, hideLabel: true,
@ -139,7 +157,7 @@ const tabs = computed(() => {
} }
}) })
return [{ no_tabs: true, sections: _sections }] return _tabs
}) })
function update() { function update() {

View File

@ -20,7 +20,9 @@
: 'ml-2 w-auto opacity-100' : 'ml-2 w-auto opacity-100'
" "
> >
<div class="text-base font-medium leading-none text-ink-gray-9 truncate"> <div
class="text-base font-medium leading-none text-ink-gray-9 truncate"
>
{{ __(brand.name || 'CRM') }} {{ __(brand.name || 'CRM') }}
</div> </div>
<div class="mt-1 text-sm leading-none text-ink-gray-7 truncate"> <div class="mt-1 text-sm leading-none text-ink-gray-7 truncate">
@ -55,7 +57,7 @@ import { getSettings } from '@/stores/settings'
import { showSettings } from '@/composables/settings' import { showSettings } from '@/composables/settings'
import { Dropdown } from 'frappe-ui' import { Dropdown } from 'frappe-ui'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { computed, markRaw, onMounted } from 'vue' import { computed, h, markRaw, onMounted } from 'vue'
const props = defineProps({ const props = defineProps({
isCollapsed: { isCollapsed: {
@ -104,22 +106,22 @@ const dropdownItems = computed(() => {
}) })
function dropdownItemObj(item) { function dropdownItemObj(item) {
let openInNewWindow = item.open_in_new_window let _item = JSON.parse(JSON.stringify(item))
let icon = _item.icon || 'external-link'
let icon = item.icon || 'external-link'
if (typeof icon === 'string' && icon.startsWith('<svg')) { if (typeof icon === 'string' && icon.startsWith('<svg')) {
icon = markRaw({ template: icon }) icon = markRaw(h('div', { innerHTML: icon }))
} }
item.icon = icon _item.icon = icon
if (item.is_standard) { if (_item.is_standard) {
return getStandardItem(item) return getStandardItem(_item)
} }
return { return {
icon: item.icon, icon: _item.icon,
label: __(item.label), label: __(_item.label),
onClick: () => window.open(item.url, openInNewWindow ? '_blank' : ''), onClick: () =>
window.open(_item.route, _item.open_in_new_window ? '_blank' : ''),
} }
} }
@ -133,13 +135,15 @@ function getStandardItem(item) {
return { return {
icon: item.icon, icon: item.icon,
label: __(item.label), label: __(item.label),
onClick: () => window.open(item.route, '_blank'), onClick: () =>
window.open(item.route, item.open_in_new_window ? '_blank' : ''),
} }
case 'docs_link': case 'docs_link':
return { return {
icon: item.icon, icon: item.icon,
label: __(item.label), label: __(item.label),
onClick: () => window.open(item.route, '_blank'), onClick: () =>
window.open(item.route, item.open_in_new_window ? '_blank' : ''),
} }
case 'toggle_theme': case 'toggle_theme':
return { return {

View File

@ -1,6 +1,6 @@
import { createResource } from 'frappe-ui' import { createResource } from 'frappe-ui'
import { formatCurrency, formatNumber } from '@/utils/numberFormat.js' import { formatCurrency, formatNumber } from '@/utils/numberFormat.js'
import { ref, reactive } from 'vue' import { reactive } from 'vue'
const doctypeMeta = reactive({}) const doctypeMeta = reactive({})
const userSettings = reactive({}) const userSettings = reactive({})
@ -55,9 +55,13 @@ export function getMeta(doctype) {
return formatCurrency(doc[fieldname], '', currency, precision) return formatCurrency(doc[fieldname], '', currency, precision)
} }
function getGridSettings(parentDoctype, dt = null) { function getGridSettings() {
return doctypeMeta[doctype] || {}
}
function getGridViewSettings(parentDoctype, dt = null) {
dt = dt || doctype dt = dt || doctype
if (!userSettings[parentDoctype]['GridView']?.[doctype]) return {} if (!userSettings[parentDoctype]?.['GridView']?.[doctype]) return {}
return userSettings[parentDoctype]['GridView'][doctype] return userSettings[parentDoctype]['GridView'][doctype]
} }
@ -77,7 +81,7 @@ export function getMeta(doctype) {
} }
function saveUserSettings(parentDoctype, key, value, callback) { function saveUserSettings(parentDoctype, key, value, callback) {
let oldUserSettings = userSettings[parentDoctype] let oldUserSettings = userSettings[parentDoctype] || {}
let newUserSettings = JSON.parse(JSON.stringify(oldUserSettings)) let newUserSettings = JSON.parse(JSON.stringify(oldUserSettings))
if (newUserSettings[key] === undefined) { if (newUserSettings[key] === undefined) {
@ -94,9 +98,13 @@ export function getMeta(doctype) {
user_settings: JSON.stringify(newUserSettings), user_settings: JSON.stringify(newUserSettings),
}, },
auto: true, auto: true,
onSuccess: () => callback?.(), onSuccess: () => {
userSettings[parentDoctype] = newUserSettings
callback?.()
},
}) })
} }
userSettings[parentDoctype] = newUserSettings
return callback?.() return callback?.()
} }
@ -106,6 +114,7 @@ export function getMeta(doctype) {
userSettings, userSettings,
getFields, getFields,
getGridSettings, getGridSettings,
getGridViewSettings,
saveUserSettings, saveUserSettings,
getFormattedFloat, getFormattedFloat,
getFormattedPercent, getFormattedPercent,

View File

@ -5,20 +5,26 @@ const settings = ref({})
const brand = reactive({}) const brand = reactive({})
export function getSettings() { export function getSettings() {
createDocumentResource({ const _settings = createDocumentResource({
doctype: 'FCRM Settings', doctype: 'FCRM Settings',
name: 'FCRM Settings', name: 'FCRM Settings',
onSuccess: (data) => { onSuccess: (data) => {
settings.value = data settings.value = data
brand.name = settings.value?.brand_name setupBrand()
brand.logo = settings.value?.brand_logo
brand.favicon = settings.value?.favicon
return data return data
}, },
}) })
function setupBrand() {
brand.name = settings.value?.brand_name
brand.logo = settings.value?.brand_logo
brand.favicon = settings.value?.favicon
}
return { return {
_settings,
settings, settings,
brand, brand,
setupBrand,
} }
} }