snapshot of https://github.com/frappe/crm.git (develop, 2026-09-18; blobs > 131072B omitted)
Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
This commit is contained in:
204
frontend/src/components/Controls/Link.vue
Normal file
204
frontend/src/components/Controls/Link.vue
Normal file
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div class="space-y-1.5 p-[2px] !-m-[2px]">
|
||||
<label v-if="attrs.label" class="block" :class="labelClasses">
|
||||
{{ __(attrs.label) }}
|
||||
</label>
|
||||
<Autocomplete
|
||||
ref="autocomplete"
|
||||
v-model="value"
|
||||
:options="options.data"
|
||||
:size="attrs.size || 'sm'"
|
||||
:variant="attrs.variant"
|
||||
:placeholder="attrs.placeholder"
|
||||
:disabled="attrs.disabled"
|
||||
:placement="attrs.placement"
|
||||
:filterable="false"
|
||||
>
|
||||
<template #target="{ open, togglePopover }">
|
||||
<slot name="target" v-bind="{ open, togglePopover }" />
|
||||
</template>
|
||||
|
||||
<template #prefix>
|
||||
<slot name="prefix" />
|
||||
</template>
|
||||
|
||||
<template #item-prefix="{ active, selected, option }">
|
||||
<slot name="item-prefix" v-bind="{ active, selected, option }" />
|
||||
</template>
|
||||
|
||||
<template #item-label="{ active, selected, option }">
|
||||
<slot name="item-label" v-bind="{ active, selected, option }">
|
||||
<div v-if="option.description" class="flex flex-col gap-1">
|
||||
<div class="flex-1 font-semibold truncate text-ink-gray-7">
|
||||
{{ option.label }}
|
||||
</div>
|
||||
<div class="flex-1 text-sm truncate text-ink-gray-5">
|
||||
{{ option.description }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex-1 truncate text-ink-gray-7">
|
||||
{{ option.label }}
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template #footer="{ value: v, close }">
|
||||
<div v-if="attrs.onCreate">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full !justify-start"
|
||||
:label="__('Create New')"
|
||||
iconLeft="plus"
|
||||
@click="() => attrs.onCreate(v, close)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full !justify-start"
|
||||
:label="__('Clear')"
|
||||
iconLeft="x"
|
||||
@click="() => clearValue(close)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Autocomplete>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
||||
import { isTranslatable } from '@/utils'
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import { createResource } from 'frappe-ui'
|
||||
import { useAttrs, computed, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
doctype: { type: String, required: true },
|
||||
filters: { type: [Array, Object, String], default: () => [] },
|
||||
modelValue: { type: String, default: '' },
|
||||
hideMe: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const attrs = useAttrs()
|
||||
|
||||
const valuePropPassed = computed(() => 'value' in attrs)
|
||||
|
||||
const value = computed({
|
||||
get: () => {
|
||||
let v = valuePropPassed.value ? attrs.value : props.modelValue
|
||||
|
||||
if (isTranslatable(props.doctype)) return __(v)
|
||||
return v
|
||||
},
|
||||
set: (val) => {
|
||||
return (
|
||||
val?.value &&
|
||||
emit(valuePropPassed.value ? 'change' : 'update:modelValue', val?.value)
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const autocomplete = ref(null)
|
||||
const text = ref('')
|
||||
|
||||
watchDebounced(
|
||||
() => autocomplete.value?.query,
|
||||
(val) => {
|
||||
val = val || ''
|
||||
if (text.value === val) return
|
||||
text.value = val
|
||||
reload(val)
|
||||
},
|
||||
{ debounce: 300, immediate: true },
|
||||
)
|
||||
|
||||
watchDebounced(
|
||||
() => props.doctype,
|
||||
() => reload(''),
|
||||
{ debounce: 300, immediate: true },
|
||||
)
|
||||
|
||||
watchDebounced(
|
||||
() => props.filters,
|
||||
() => {
|
||||
reload('', true)
|
||||
},
|
||||
{ debounce: 300, immediate: true },
|
||||
)
|
||||
|
||||
const options = createResource({
|
||||
url: 'frappe.desk.search.search_link',
|
||||
cache: [props.doctype, text.value, props.hideMe, props.filters],
|
||||
method: 'POST',
|
||||
params: {
|
||||
txt: text.value,
|
||||
doctype: props.doctype,
|
||||
filters: props.filters,
|
||||
},
|
||||
transform: (data) => {
|
||||
let allData = data.map((option) => {
|
||||
return {
|
||||
label: option.label || option.value,
|
||||
value: option.value,
|
||||
description: stripHtml(option.description),
|
||||
}
|
||||
})
|
||||
if (!props.hideMe && props.doctype == 'User') {
|
||||
allData.unshift({
|
||||
label: '@me',
|
||||
value: '@me',
|
||||
})
|
||||
}
|
||||
return allData
|
||||
},
|
||||
})
|
||||
|
||||
function stripHtml(html) {
|
||||
if (!html) return ''
|
||||
return html
|
||||
.replace(/<[^>]*>/g, ' ')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function reload(val, force = false) {
|
||||
if (!props.doctype) return
|
||||
if (
|
||||
!force &&
|
||||
options.data?.length &&
|
||||
val === options.params?.txt &&
|
||||
props.doctype === options.params?.doctype
|
||||
)
|
||||
return
|
||||
|
||||
options.update({
|
||||
params: {
|
||||
txt: val,
|
||||
doctype: props.doctype,
|
||||
filters: props.filters,
|
||||
},
|
||||
})
|
||||
options.reload()
|
||||
}
|
||||
|
||||
function clearValue(close) {
|
||||
emit(valuePropPassed.value ? 'change' : 'update:modelValue', '')
|
||||
close()
|
||||
}
|
||||
|
||||
const labelClasses = computed(() => {
|
||||
return [
|
||||
{
|
||||
sm: 'text-xs',
|
||||
md: 'text-base',
|
||||
}[attrs.size || 'sm'],
|
||||
'text-ink-gray-5',
|
||||
]
|
||||
})
|
||||
|
||||
defineExpose({ reload })
|
||||
</script>
|
||||
Reference in New Issue
Block a user