2
0
mirror of https://github.com/frappe/books.git synced 2024-12-22 19:09:01 +00:00

feat: add autocomplete for linklabels

- update editor styling
This commit is contained in:
18alantom 2023-03-09 11:47:25 +05:30
parent ec9cc7f2b4
commit 11c714a957
5 changed files with 150 additions and 19 deletions

View File

@ -20,6 +20,7 @@
"test": "scripts/test.sh"
},
"dependencies": {
"@codemirror/autocomplete": "^6.4.2",
"@codemirror/lang-vue": "^0.1.1",
"@popperjs/core": "^2.10.2",
"better-sqlite3": "^7.5.3",

View File

@ -178,7 +178,7 @@
>
<div class="flex">
<!-- 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">
{{ t`Value Keys` }}
</h2>
@ -186,7 +186,7 @@
class="overflow-auto custom-scroll p-4"
style="max-height: 80vh; width: 25vw"
>
<TemplateBuilderHint :hint="hint" />
<TemplateBuilderHint :hints="hints" />
</div>
</div>
@ -196,11 +196,12 @@
{{ t`Template` }}
</h2>
<TemplateEditor
class="overflow-auto custom-scroll"
style="max-height: 80vh; width: 65vw"
v-if="!templateCollapsed && typeof doc.template === 'string'"
v-model.lazy="doc.template"
:disabled="!doc.isCustom"
:hints="hints ?? undefined"
class="overflow-auto custom-scroll"
style="max-height: 80vh; width: 65vw"
/>
</div>
</div>
@ -253,14 +254,14 @@ export default defineComponent({
return {
doc: null,
showEditor: false,
hint: null,
hints: undefined,
values: null,
templateCollapsed: false,
helpersCollapsed: true,
displayDoc: null,
scale: 0.65,
} as {
hint: null | Record<string, unknown>;
hints?: Record<string, unknown>;
values: null | PrintValues;
doc: PrintTemplate | null;
showEditor: boolean;
@ -344,7 +345,7 @@ export default defineComponent({
},
async setDisplayDoc(value: string) {
if (!value) {
this.hint = null;
delete this.hints;
this.values = null;
this.displayDoc = null;
return;
@ -356,7 +357,7 @@ export default defineComponent({
}
const displayDoc = await getDocFromNameIfExistsElseNew(schemaName, value);
this.hint = getPrintTemplatePropHints(displayDoc);
this.hints = getPrintTemplatePropHints(displayDoc);
this.values = await getPrintTemplatePropValues(displayDoc);
this.displayDoc = displayDoc;
},

View File

@ -55,7 +55,7 @@
<div v-if="!r.collapsed && typeof r.value === 'object'">
<TemplateBuilderHint
: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"
/>
</div>
@ -74,7 +74,7 @@ export default defineComponent({
name: 'TemplateBuilderHint',
props: {
prefix: { type: String, default: '' },
hint: { type: Object, required: true },
hints: { type: Object, required: true },
level: { type: Number, default: 0 },
},
data() {
@ -83,7 +83,7 @@ export default defineComponent({
};
},
mounted() {
this.rows = Object.entries(this.hint)
this.rows = Object.entries(this.hints)
.map(([key, value]) => ({
key,
value,

View File

@ -2,8 +2,13 @@
<div ref="container" class="bg-white text-gray-900"></div>
</template>
<script lang="ts">
import { autocompletion, CompletionContext } from '@codemirror/autocomplete';
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 { EditorView, ViewUpdate } from '@codemirror/view';
import { tags } from '@lezer/highlight';
@ -24,6 +29,7 @@ export default defineComponent({
modelModifiers: { type: Object, default: () => ({}) },
modelValue: { type: String, required: true },
disabled: { type: Boolean, default: false },
hints: { type: Object },
},
watch: {
disabled(value: boolean) {
@ -44,14 +50,15 @@ export default defineComponent({
const highlightStyle = HighlightStyle.define([
{ tag: tags.typeName, color: uicolors.pink[600] },
{ tag: tags.angleBracket, color: uicolors.pink[600] },
{ tag: tags.attributeName, color: uicolors.gray[700] },
{ tag: tags.attributeValue, color: uicolors.blue[700] },
{ tag: tags.comment, color: uicolors.gray[500] },
{ tag: tags.keyword, color: uicolors.blue[500] },
{ tag: tags.variableName, color: uicolors.yellow[600] },
{ tag: tags.string, color: uicolors.pink[700] },
{ tag: tags.attributeName, color: uicolors.gray[600] },
{ tag: tags.attributeValue, color: uicolors.blue[600] },
{ tag: tags.comment, color: uicolors.gray[500], fontStyle: 'italic' },
{ tag: tags.keyword, color: uicolors.pink[500] },
{ tag: tags.variableName, color: uicolors.blue[700] },
{ tag: tags.string, color: uicolors.pink[600] },
{ tag: tags.content, color: uicolors.gray[700] },
]);
const completions = getCompletionsFromHints(this.hints ?? {});
const view = new EditorView({
doc: this.modelValue,
@ -62,6 +69,7 @@ export default defineComponent({
basicSetup,
vue(),
syntaxHighlighting(highlightStyle),
autocompletion({ override: [completions] }),
],
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>
<style>
.cm-line {
font-weight: 600;
}
.cm-gutter {
@apply bg-gray-50;
}
@ -121,4 +222,32 @@ export default defineComponent({
.cm-activeLineGutter {
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>

View File

@ -962,7 +962,7 @@
"@babel/helper-validator-identifier" "^7.15.7"
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"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.4.2.tgz#938b25223bd21f97b2a6d85474643355f98b505b"
integrity sha512-8WE2xp+D0MpWEv5lZ6zPW1/tf4AGb358T5GWYiKEuCP8MvFfT3tH2mIF9Y2yr2e3KbHuSvsVhosiEyqCpiJhZQ==