mirror of
https://github.com/frappe/books.git
synced 2025-01-08 17:24:05 +00:00
feat: add autocomplete for linklabels
- update editor styling
This commit is contained in:
parent
ec9cc7f2b4
commit
11c714a957
@ -20,6 +20,7 @@
|
|||||||
"test": "scripts/test.sh"
|
"test": "scripts/test.sh"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.4.2",
|
||||||
"@codemirror/lang-vue": "^0.1.1",
|
"@codemirror/lang-vue": "^0.1.1",
|
||||||
"@popperjs/core": "^2.10.2",
|
"@popperjs/core": "^2.10.2",
|
||||||
"better-sqlite3": "^7.5.3",
|
"better-sqlite3": "^7.5.3",
|
||||||
|
@ -178,7 +178,7 @@
|
|||||||
>
|
>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<!-- Hint Section Header -->
|
<!-- Hint Section Header -->
|
||||||
<div class="border-r" v-if="hint">
|
<div class="border-r" v-if="hints">
|
||||||
<h2 class="text-base font-semibold p-4 border-b">
|
<h2 class="text-base font-semibold p-4 border-b">
|
||||||
{{ t`Value Keys` }}
|
{{ t`Value Keys` }}
|
||||||
</h2>
|
</h2>
|
||||||
@ -186,7 +186,7 @@
|
|||||||
class="overflow-auto custom-scroll p-4"
|
class="overflow-auto custom-scroll p-4"
|
||||||
style="max-height: 80vh; width: 25vw"
|
style="max-height: 80vh; width: 25vw"
|
||||||
>
|
>
|
||||||
<TemplateBuilderHint :hint="hint" />
|
<TemplateBuilderHint :hints="hints" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -196,11 +196,12 @@
|
|||||||
{{ t`Template` }}
|
{{ t`Template` }}
|
||||||
</h2>
|
</h2>
|
||||||
<TemplateEditor
|
<TemplateEditor
|
||||||
class="overflow-auto custom-scroll"
|
|
||||||
style="max-height: 80vh; width: 65vw"
|
|
||||||
v-if="!templateCollapsed && typeof doc.template === 'string'"
|
v-if="!templateCollapsed && typeof doc.template === 'string'"
|
||||||
v-model.lazy="doc.template"
|
v-model.lazy="doc.template"
|
||||||
:disabled="!doc.isCustom"
|
:disabled="!doc.isCustom"
|
||||||
|
:hints="hints ?? undefined"
|
||||||
|
class="overflow-auto custom-scroll"
|
||||||
|
style="max-height: 80vh; width: 65vw"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -253,14 +254,14 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
doc: null,
|
doc: null,
|
||||||
showEditor: false,
|
showEditor: false,
|
||||||
hint: null,
|
hints: undefined,
|
||||||
values: null,
|
values: null,
|
||||||
templateCollapsed: false,
|
templateCollapsed: false,
|
||||||
helpersCollapsed: true,
|
helpersCollapsed: true,
|
||||||
displayDoc: null,
|
displayDoc: null,
|
||||||
scale: 0.65,
|
scale: 0.65,
|
||||||
} as {
|
} as {
|
||||||
hint: null | Record<string, unknown>;
|
hints?: Record<string, unknown>;
|
||||||
values: null | PrintValues;
|
values: null | PrintValues;
|
||||||
doc: PrintTemplate | null;
|
doc: PrintTemplate | null;
|
||||||
showEditor: boolean;
|
showEditor: boolean;
|
||||||
@ -344,7 +345,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
async setDisplayDoc(value: string) {
|
async setDisplayDoc(value: string) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
this.hint = null;
|
delete this.hints;
|
||||||
this.values = null;
|
this.values = null;
|
||||||
this.displayDoc = null;
|
this.displayDoc = null;
|
||||||
return;
|
return;
|
||||||
@ -356,7 +357,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const displayDoc = await getDocFromNameIfExistsElseNew(schemaName, value);
|
const displayDoc = await getDocFromNameIfExistsElseNew(schemaName, value);
|
||||||
this.hint = getPrintTemplatePropHints(displayDoc);
|
this.hints = getPrintTemplatePropHints(displayDoc);
|
||||||
this.values = await getPrintTemplatePropValues(displayDoc);
|
this.values = await getPrintTemplatePropValues(displayDoc);
|
||||||
this.displayDoc = displayDoc;
|
this.displayDoc = displayDoc;
|
||||||
},
|
},
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
<div v-if="!r.collapsed && typeof r.value === 'object'">
|
<div v-if="!r.collapsed && typeof r.value === 'object'">
|
||||||
<TemplateBuilderHint
|
<TemplateBuilderHint
|
||||||
:prefix="getKey(r)"
|
:prefix="getKey(r)"
|
||||||
:hint="Array.isArray(r.value) ? r.value[0] : r.value"
|
:hints="Array.isArray(r.value) ? r.value[0] : r.value"
|
||||||
:level="level + 1"
|
:level="level + 1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -74,7 +74,7 @@ export default defineComponent({
|
|||||||
name: 'TemplateBuilderHint',
|
name: 'TemplateBuilderHint',
|
||||||
props: {
|
props: {
|
||||||
prefix: { type: String, default: '' },
|
prefix: { type: String, default: '' },
|
||||||
hint: { type: Object, required: true },
|
hints: { type: Object, required: true },
|
||||||
level: { type: Number, default: 0 },
|
level: { type: Number, default: 0 },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -83,7 +83,7 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.rows = Object.entries(this.hint)
|
this.rows = Object.entries(this.hints)
|
||||||
.map(([key, value]) => ({
|
.map(([key, value]) => ({
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
|
@ -2,8 +2,13 @@
|
|||||||
<div ref="container" class="bg-white text-gray-900"></div>
|
<div ref="container" class="bg-white text-gray-900"></div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { autocompletion, CompletionContext } from '@codemirror/autocomplete';
|
||||||
import { vue } from '@codemirror/lang-vue';
|
import { vue } from '@codemirror/lang-vue';
|
||||||
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
import {
|
||||||
|
HighlightStyle,
|
||||||
|
syntaxHighlighting,
|
||||||
|
syntaxTree,
|
||||||
|
} from '@codemirror/language';
|
||||||
import { Compartment, EditorState } from '@codemirror/state';
|
import { Compartment, EditorState } from '@codemirror/state';
|
||||||
import { EditorView, ViewUpdate } from '@codemirror/view';
|
import { EditorView, ViewUpdate } from '@codemirror/view';
|
||||||
import { tags } from '@lezer/highlight';
|
import { tags } from '@lezer/highlight';
|
||||||
@ -24,6 +29,7 @@ export default defineComponent({
|
|||||||
modelModifiers: { type: Object, default: () => ({}) },
|
modelModifiers: { type: Object, default: () => ({}) },
|
||||||
modelValue: { type: String, required: true },
|
modelValue: { type: String, required: true },
|
||||||
disabled: { type: Boolean, default: false },
|
disabled: { type: Boolean, default: false },
|
||||||
|
hints: { type: Object },
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
disabled(value: boolean) {
|
disabled(value: boolean) {
|
||||||
@ -44,14 +50,15 @@ export default defineComponent({
|
|||||||
const highlightStyle = HighlightStyle.define([
|
const highlightStyle = HighlightStyle.define([
|
||||||
{ tag: tags.typeName, color: uicolors.pink[600] },
|
{ tag: tags.typeName, color: uicolors.pink[600] },
|
||||||
{ tag: tags.angleBracket, color: uicolors.pink[600] },
|
{ tag: tags.angleBracket, color: uicolors.pink[600] },
|
||||||
{ tag: tags.attributeName, color: uicolors.gray[700] },
|
{ tag: tags.attributeName, color: uicolors.gray[600] },
|
||||||
{ tag: tags.attributeValue, color: uicolors.blue[700] },
|
{ tag: tags.attributeValue, color: uicolors.blue[600] },
|
||||||
{ tag: tags.comment, color: uicolors.gray[500] },
|
{ tag: tags.comment, color: uicolors.gray[500], fontStyle: 'italic' },
|
||||||
{ tag: tags.keyword, color: uicolors.blue[500] },
|
{ tag: tags.keyword, color: uicolors.pink[500] },
|
||||||
{ tag: tags.variableName, color: uicolors.yellow[600] },
|
{ tag: tags.variableName, color: uicolors.blue[700] },
|
||||||
{ tag: tags.string, color: uicolors.pink[700] },
|
{ tag: tags.string, color: uicolors.pink[600] },
|
||||||
{ tag: tags.content, color: uicolors.gray[700] },
|
{ tag: tags.content, color: uicolors.gray[700] },
|
||||||
]);
|
]);
|
||||||
|
const completions = getCompletionsFromHints(this.hints ?? {});
|
||||||
|
|
||||||
const view = new EditorView({
|
const view = new EditorView({
|
||||||
doc: this.modelValue,
|
doc: this.modelValue,
|
||||||
@ -62,6 +69,7 @@ export default defineComponent({
|
|||||||
basicSetup,
|
basicSetup,
|
||||||
vue(),
|
vue(),
|
||||||
syntaxHighlighting(highlightStyle),
|
syntaxHighlighting(highlightStyle),
|
||||||
|
autocompletion({ override: [completions] }),
|
||||||
],
|
],
|
||||||
parent: this.container,
|
parent: this.container,
|
||||||
});
|
});
|
||||||
@ -107,8 +115,101 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getCompletionsFromHints(hints: Record<string, unknown>) {
|
||||||
|
const options = hintsToCompletionOptions(hints);
|
||||||
|
return function completions(context: CompletionContext) {
|
||||||
|
let word = context.matchBefore(/\w*/);
|
||||||
|
if (word == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = syntaxTree(context.state).resolveInner(context.pos);
|
||||||
|
const aptLocation = ['ScriptAttributeValue', 'SingleExpression'];
|
||||||
|
|
||||||
|
if (!aptLocation.includes(node.name)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (word.from === word.to && !context.explicit) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: word.from,
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompletionOption = {
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
detail: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function hintsToCompletionOptions(
|
||||||
|
hints: object,
|
||||||
|
prefix?: string
|
||||||
|
): CompletionOption[] {
|
||||||
|
prefix ??= '';
|
||||||
|
const list: CompletionOption[] = [];
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(hints)) {
|
||||||
|
const option = getCompletionOption(key, value, prefix);
|
||||||
|
if (option === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(option)) {
|
||||||
|
list.push(...option);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.push(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCompletionOption(
|
||||||
|
key: string,
|
||||||
|
value: unknown,
|
||||||
|
prefix: string
|
||||||
|
): null | CompletionOption | CompletionOption[] {
|
||||||
|
let label = key;
|
||||||
|
if (prefix.length) {
|
||||||
|
label = prefix + '.' + key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
type: 'variable',
|
||||||
|
detail: 'Child Table',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
type: 'variable',
|
||||||
|
detail: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
return hintsToCompletionOptions(value, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
|
.cm-line {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.cm-gutter {
|
.cm-gutter {
|
||||||
@apply bg-gray-50;
|
@apply bg-gray-50;
|
||||||
}
|
}
|
||||||
@ -121,4 +222,32 @@ export default defineComponent({
|
|||||||
.cm-activeLineGutter {
|
.cm-activeLineGutter {
|
||||||
background-color: #e5f3ff67 !important;
|
background-color: #e5f3ff67 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cm-tooltip-autocomplete {
|
||||||
|
background-color: white !important;
|
||||||
|
border: 1px solid theme('colors.gray.200') !important;
|
||||||
|
@apply rounded shadow-lg overflow-hidden text-gray-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-tooltip-autocomplete [aria-selected] {
|
||||||
|
color: #334155 !important;
|
||||||
|
background-color: theme('colors.blue.100') !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-panels {
|
||||||
|
border-top: 1px solid theme('colors.gray.200') !important;
|
||||||
|
background-color: theme('colors.gray.50') !important;
|
||||||
|
color: theme('colors.gray.800') !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-button {
|
||||||
|
background-image: none !important;
|
||||||
|
background-color: theme('colors.gray.200') !important;
|
||||||
|
color: theme('colors.gray.700') !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-textfield {
|
||||||
|
border: 1px solid theme('colors.gray.200') !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -962,7 +962,7 @@
|
|||||||
"@babel/helper-validator-identifier" "^7.15.7"
|
"@babel/helper-validator-identifier" "^7.15.7"
|
||||||
to-fast-properties "^2.0.0"
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
"@codemirror/autocomplete@^6.0.0":
|
"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.4.2":
|
||||||
version "6.4.2"
|
version "6.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.4.2.tgz#938b25223bd21f97b2a6d85474643355f98b505b"
|
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.4.2.tgz#938b25223bd21f97b2a6d85474643355f98b505b"
|
||||||
integrity sha512-8WE2xp+D0MpWEv5lZ6zPW1/tf4AGb358T5GWYiKEuCP8MvFfT3tH2mIF9Y2yr2e3KbHuSvsVhosiEyqCpiJhZQ==
|
integrity sha512-8WE2xp+D0MpWEv5lZ6zPW1/tf4AGb358T5GWYiKEuCP8MvFfT3tH2mIF9Y2yr2e3KbHuSvsVhosiEyqCpiJhZQ==
|
||||||
|
Loading…
Reference in New Issue
Block a user