queryeditor.vue 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. <template>
  2. <div class="opacity-0 show-if-initialized" :class="{ initialized }">
  3. <pre ref="editor"></pre>
  4. </div>
  5. </template>
  6. <style scoped lang="scss">
  7. .show-if-initialized {
  8. &.initialized {
  9. @apply opacity-100;
  10. }
  11. & > * {
  12. @apply transition-none;
  13. }
  14. }
  15. </style>
  16. <script>
  17. import ace from "ace-builds"
  18. import "ace-builds/webpack-resolver"
  19. import "ace-builds/src-noconflict/ext-language_tools"
  20. import "ace-builds/src-noconflict/mode-graphqlschema"
  21. import { defineGQLLanguageMode } from "~/helpers/syntax/gqlQueryLangMode"
  22. import * as gql from "graphql"
  23. import { getAutocompleteSuggestions } from "graphql-language-service-interface"
  24. import debounce from "~/helpers/utils/debounce"
  25. export default {
  26. props: {
  27. value: {
  28. type: String,
  29. default: "",
  30. },
  31. theme: {
  32. type: String,
  33. required: false,
  34. default: null,
  35. },
  36. onRunGQLQuery: {
  37. type: Function,
  38. default: () => {},
  39. },
  40. options: {
  41. type: Object,
  42. default: {},
  43. },
  44. },
  45. data() {
  46. return {
  47. initialized: false,
  48. editor: null,
  49. cacheValue: "",
  50. validationSchema: null,
  51. }
  52. },
  53. watch: {
  54. value(value) {
  55. if (value !== this.cacheValue) {
  56. this.editor.session.setValue(value, 1)
  57. this.cacheValue = value
  58. }
  59. },
  60. theme() {
  61. this.initialized = false
  62. this.editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
  63. this.$nextTick().then(() => {
  64. this.initialized = true
  65. })
  66. })
  67. },
  68. options(value) {
  69. this.editor.setOptions(value)
  70. },
  71. },
  72. mounted() {
  73. defineGQLLanguageMode(ace)
  74. let langTools = ace.require("ace/ext/language_tools")
  75. const editor = ace.edit(this.$refs.editor, {
  76. mode: `ace/mode/gql-query`,
  77. enableBasicAutocompletion: true,
  78. enableLiveAutocompletion: true,
  79. ...this.options,
  80. })
  81. // Set the theme and show the editor only after it's been set to prevent FOUC.
  82. editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
  83. this.$nextTick().then(() => {
  84. this.initialized = true
  85. })
  86. })
  87. // Set the theme and show the editor only after it's been set to prevent FOUC.
  88. editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
  89. this.$nextTick().then(() => {
  90. this.initialized = true
  91. })
  92. })
  93. const completer = {
  94. getCompletions: (editor, _session, { row, column }, _prefix, callback) => {
  95. if (this.validationSchema) {
  96. const completions = getAutocompleteSuggestions(this.validationSchema, editor.getValue(), {
  97. line: row,
  98. character: column,
  99. })
  100. callback(
  101. null,
  102. completions.map(({ label, detail }) => ({
  103. name: label,
  104. value: label,
  105. score: 1.0,
  106. meta: detail,
  107. }))
  108. )
  109. } else {
  110. callback(null, [])
  111. }
  112. },
  113. }
  114. langTools.setCompleters([completer])
  115. if (this.value) editor.setValue(this.value, 1)
  116. this.editor = editor
  117. this.cacheValue = this.value
  118. editor.commands.addCommand({
  119. name: "runGQLQuery",
  120. exec: () => this.onRunGQLQuery(this.editor.getValue()),
  121. bindKey: {
  122. mac: "cmd-enter",
  123. win: "ctrl-enter",
  124. },
  125. })
  126. editor.commands.addCommand({
  127. name: "prettifyGQLQuery",
  128. exec: () => this.prettifyQuery(),
  129. bindKey: {
  130. mac: "cmd-p",
  131. win: "ctrl-p",
  132. },
  133. })
  134. editor.on("change", () => {
  135. const content = editor.getValue()
  136. this.$emit("input", content)
  137. this.parseContents(content)
  138. this.cacheValue = content
  139. })
  140. this.parseContents(this.value)
  141. },
  142. methods: {
  143. prettifyQuery() {
  144. try {
  145. this.value = gql.print(gql.parse(this.editor.getValue()))
  146. } catch (e) {
  147. this.$toast.error(`${this.$t("gql_prettify_invalid_query")}`, {
  148. icon: "error",
  149. })
  150. }
  151. },
  152. defineTheme() {
  153. if (this.theme) {
  154. return this.theme
  155. }
  156. const strip = (str) => str.replace(/#/g, "").replace(/ /g, "").replace(/"/g, "")
  157. return strip(
  158. window.getComputedStyle(document.documentElement).getPropertyValue("--editor-theme")
  159. )
  160. },
  161. setValidationSchema(schema) {
  162. this.validationSchema = schema
  163. this.parseContents(this.cacheValue)
  164. },
  165. parseContents: debounce(function (content) {
  166. if (content !== "") {
  167. try {
  168. const doc = gql.parse(content)
  169. if (this.validationSchema) {
  170. this.editor.session.setAnnotations(
  171. gql.validate(this.validationSchema, doc).map(({ locations, message }) => ({
  172. row: locations[0].line - 1,
  173. column: locations[0].column - 1,
  174. text: message,
  175. type: "error",
  176. }))
  177. )
  178. }
  179. } catch (e) {
  180. this.editor.session.setAnnotations([
  181. {
  182. row: e.locations[0].line - 1,
  183. column: e.locations[0].column - 1,
  184. text: e.message,
  185. type: "error",
  186. },
  187. ])
  188. }
  189. } else {
  190. this.editor.session.setAnnotations([])
  191. }
  192. }, 2000),
  193. },
  194. beforeDestroy() {
  195. this.editor.destroy()
  196. },
  197. }
  198. </script>