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 []:
field = next((f for f in fields if f.fieldname == field), None)
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 = {
"label": _(field.label),
"name": field.fieldname,
"type": field.fieldtype,
"options": field.options,
"options": getOptions(field),
"mandatory": field.reqd,
"read_only": field.read_only,
"placeholder": field.get("placeholder"),
"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):
fields = frappe.get_meta(doctype).fields
fields = [
{
"label": _(field.label),
"name": field.fieldname,
"type": field.fieldtype,
"options": field.options,
"mandatory": field.reqd,
"placeholder": field.get("placeholder"),
"filters": field.get("link_filters"),
}
field.fieldname
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}]}]
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>
<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">
{{ __(label) }}
</div>
@ -55,9 +55,16 @@
<template #item="{ element: row, index }">
<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"
@click.stop="
() => {
if (!gridSettings.editable_grid) {
showRowList[index] = true
}
}
"
>
<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
class="cursor-pointer duration-300"
@ -66,7 +73,7 @@
/>
</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 }}
</div>
@ -88,11 +95,12 @@
/>
<div
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
class="cursor-pointer duration-300"
v-model="row[field.name]"
:disabled="!gridSettings.editable_grid"
/>
</div>
<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 showRowList = ref(new Array(rows.value?.length || []).fill(false))
@ -242,11 +253,13 @@ const selectedRows = reactive(new Set())
const showGridFieldsEditorModal = ref(false)
const showGridRowFieldsModal = ref(false)
const gridSettings = computed(() => getGridSettings())
const fields = computed(() => {
let gridSettings = getGridSettings(props.parentDoctype)
let gridViewSettings = getGridViewSettings(props.parentDoctype)
let gridFields = getFields()
if (gridSettings.length) {
let d = gridSettings.map((gs) =>
if (gridViewSettings.length) {
let d = gridViewSettings.map((gs) =>
getFieldObj(gridFields.find((f) => f.fieldname === gs.fieldname)),
)
return d
@ -269,9 +282,11 @@ function getFieldObj(field) {
const gridTemplateColumns = computed(() => {
if (!fields.value?.length) return '1fr'
// for the checkbox & sr no. columns
let gridSettings = getGridSettings(props.parentDoctype)
if (gridSettings.length) {
return gridSettings.map((gs) => `minmax(0, ${gs.columns || 2}fr)`).join(' ')
let gridViewSettings = getGridViewSettings(props.parentDoctype)
if (gridViewSettings.length) {
return gridViewSettings
.map((gs) => `minmax(0, ${gs.columns || 2}fr)`)
.join(' ')
}
return fields.value.map(() => `minmax(0, 2fr)`).join(' ')
})
@ -353,6 +368,10 @@ const deleteRows = () => {
height: 38px;
}
:deep(.grid-row:last-child .grid-row-checkbox) {
border-bottom-left-radius: 7px;
}
:deep(.grid-row .edit-row button) {
border-bottom-right-radius: 7px;
}

View File

@ -107,7 +107,7 @@ const props = defineProps({
parentDoctype: String,
})
const { userSettings, getFields, getGridSettings, saveUserSettings } = getMeta(
const { getFields, getGridViewSettings, saveUserSettings } = getMeta(
props.doctype,
)
@ -122,10 +122,10 @@ const dirty = computed(() => {
const oldFields = computed(() => {
let _fields = getFields()
let gridSettings = getGridSettings(props.parentDoctype)
let gridViewSettings = getGridViewSettings(props.parentDoctype)
if (gridSettings.length) {
return gridSettings.map((field) => {
if (gridViewSettings.length) {
return gridViewSettings.map((field) => {
let f = _fields.find((f) => f.fieldname === field.fieldname)
if (f) {
f.columns = field.columns
@ -175,14 +175,6 @@ function update() {
saveUserSettings(props.parentDoctype, 'GridView', updateFields, () => {
loading.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>
import EditIcon from '@/components/Icons/EditIcon.vue'
import FieldLayout from '@/components/FieldLayout.vue'
import { getMeta } from '@/stores/meta'
import { usersStore } from '@/stores/users'
import { createResource } from 'frappe-ui'
import { nextTick } from 'vue'
@ -44,7 +43,6 @@ const props = defineProps({
doctype: String,
})
const { getFields } = getMeta(props.doctype)
const { isManager } = usersStore()
const show = defineModel()
@ -52,28 +50,9 @@ const showGridRowFieldsModal = defineModel('showGridRowFieldsModal')
const tabs = createResource({
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' },
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() {

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
v-model="tabIndex"
class="!h-full"
:tabs="tabs"
:tabs="_tabs"
v-slot="{ tab }"
:tablistClass="
!hasTabs ? 'hidden' : modal ? 'border-outline-gray-modals' : ''
@ -44,16 +44,7 @@
]"
>
<div v-for="field in section.fields" :key="field.name">
<div
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 class="settings-field">
<div
v-if="field.type != 'Check'"
class="mb-2 text-sm text-ink-gray-5"
@ -79,7 +70,6 @@
<Grid
v-else-if="field.type === 'Table'"
v-model="data[field.name]"
:fields="field.fields"
:doctype="field.options"
:parentDoctype="doctype"
/>
@ -109,7 +99,13 @@
/>
<label
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) }}
<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 _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)
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">
{{ __('Send Invites To') }}
</h2>
<div class="flex-1 overflow-y-auto">
<label class="block text-xs text-ink-gray-5 mb-1.5">
{{ __('Invite by email') }}
</label>
<MultiValueInput
v-model="invitees"
:validate="validateEmail"
:error-message="
(value) => __('{0} is an invalid email address', [value])
"
/>
<FormControl
type="select"
class="mt-4"
v-model="role"
:label="__('Invite as')"
:options="[
{ label: __('Regular Access'), value: 'Sales User' },
{ label: __('Manager Access'), value: 'Sales Manager' },
]"
:description="description"
/>
<ErrorMessage class="mt-2" v-if="error" :message="error" />
<div class="flex-1 flex flex-col gap-8 overflow-y-auto">
<div>
<label class="block text-xs text-ink-gray-5 mb-1.5">
{{ __('Invite by email') }}
</label>
<MultiValueInput
v-model="invitees"
:validate="validateEmail"
:error-message="
(value) => __('{0} is an invalid email address', [value])
"
/>
<FormControl
type="select"
class="mt-4"
v-model="role"
:label="__('Invite as')"
:options="[
{ label: __('Regular Access'), value: 'Sales User' },
{ label: __('Manager Access'), value: 'Sales Manager' },
]"
:description="description"
/>
<ErrorMessage class="mt-2" v-if="error" :message="error" />
</div>
<template v-if="pendingInvitations.data?.length && !invitees.length">
<div
class="mt-6 flex items-center justify-between py-4 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="flex flex-col gap-4">
<div
class="flex items-center justify-between text-base font-semibold"
>
<div class="text-base">
<span class="text-ink-gray-9">
{{ user.email }}
</span>
<span class="text-ink-gray-5"> ({{ roleMap[user.role] }}) </span>
</div>
<div>
<Tooltip text="Delete Invitation">
<Button
icon="x"
variant="ghost"
:loading="
pendingInvitations.delete.loading &&
pendingInvitations.delete.params.name === user.name
"
@click="pendingInvitations.delete.submit(user.name)"
/>
</Tooltip>
</div>
</li>
</ul>
<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">
<span class="text-ink-gray-9">
{{ user.email }}
</span>
<span class="text-ink-gray-5">
({{ roleMap[user.role] }})
</span>
</div>
<div>
<Tooltip text="Delete Invitation">
<Button
icon="x"
variant="ghost"
:loading="
pendingInvitations.delete.loading &&
pendingInvitations.delete.params.name === user.name
"
@click="pendingInvitations.delete.submit(user.name)"
/>
</Tooltip>
</div>
</li>
</ul>
</div>
</template>
</div>
<div class="flex flex-row-reverse">

View File

@ -33,7 +33,14 @@
</nav>
</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" />
</div>
</div>
@ -44,6 +51,7 @@
import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
import ERPNextIcon from '@/components/Icons/ERPNextIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import GeneralSettings from '@/components/Settings/GeneralSettings.vue'
import InviteMemberPage from '@/components/Settings/InviteMemberPage.vue'
import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue'
@ -56,7 +64,7 @@ import {
showSettings,
activeSettingsPage,
} from '@/composables/settings'
import { Dialog, Avatar } from 'frappe-ui'
import { Dialog, Button, Avatar } from 'frappe-ui'
import { ref, markRaw, computed, watch, h } from 'vue'
const { isManager, getUser } = usersStore()
@ -79,6 +87,12 @@ const tabs = computed(() => {
}),
component: markRaw(ProfileSettings),
},
{
label: __('General'),
icon: 'settings',
component: markRaw(GeneralSettings),
condition: () => isManager(),
},
{
label: __('Invite Members'),
icon: 'user-plus',

View File

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

View File

@ -20,7 +20,9 @@
: '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') }}
</div>
<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 { Dropdown } from 'frappe-ui'
import { useStorage } from '@vueuse/core'
import { computed, markRaw, onMounted } from 'vue'
import { computed, h, markRaw, onMounted } from 'vue'
const props = defineProps({
isCollapsed: {
@ -104,22 +106,22 @@ const dropdownItems = computed(() => {
})
function dropdownItemObj(item) {
let openInNewWindow = item.open_in_new_window
let icon = item.icon || 'external-link'
let _item = JSON.parse(JSON.stringify(item))
let icon = _item.icon || 'external-link'
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) {
return getStandardItem(item)
if (_item.is_standard) {
return getStandardItem(_item)
}
return {
icon: item.icon,
label: __(item.label),
onClick: () => window.open(item.url, openInNewWindow ? '_blank' : ''),
icon: _item.icon,
label: __(_item.label),
onClick: () =>
window.open(_item.route, _item.open_in_new_window ? '_blank' : ''),
}
}
@ -133,13 +135,15 @@ function getStandardItem(item) {
return {
icon: item.icon,
label: __(item.label),
onClick: () => window.open(item.route, '_blank'),
onClick: () =>
window.open(item.route, item.open_in_new_window ? '_blank' : ''),
}
case 'docs_link':
return {
icon: item.icon,
label: __(item.label),
onClick: () => window.open(item.route, '_blank'),
onClick: () =>
window.open(item.route, item.open_in_new_window ? '_blank' : ''),
}
case 'toggle_theme':
return {

View File

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

View File

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