Merge pull request #496 from shariquerik/crm-settings
This commit is contained in:
commit
c0732060e0
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
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
|
<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) {
|
||||||
|
|||||||
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">
|
<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">
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user