Merge pull request #497 from frappe/develop
This commit is contained in:
commit
315c77f338
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 },
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
42
frontend/src/components/Controls/ImageUploader.vue
Normal file
42
frontend/src/components/Controls/ImageUploader.vue
Normal 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>
|
||||
@ -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) {
|
||||
|
||||
133
frontend/src/components/Settings/GeneralSettings.vue
Normal file
133
frontend/src/components/Settings/GeneralSettings.vue
Normal 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>
|
||||
@ -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">
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user