Просмотр исходного кода

chore: migrate frontend linting from standard to neostandard

Replace the ESLint 8-based standard package with neostandard and a
flat config so frontend installs no longer pull deprecated ESLint 8
transitives. Keep vue recommended rules and auto-fix indent drift.

Co-authored-by: Cursor <cursoragent@cursor.com>
jamesread 22 часов назад
Родитель
Сommit
fa2702094b

+ 0 - 25
frontend/.eslintrc.json

@@ -1,25 +0,0 @@
-{
-    "env": {
-        "browser": true,
-        "es2021": true
-    },
-    "extends": [
-        "plugin:vue/recommended",
-        "standard"
-    ],
-    "parser": "vue-eslint-parser",
-    "parserOptions": {
-        "ecmaVersion": 12,
-        "sourceType": "module",
-        "parser": {
-            "js": "espree"
-        }
-    },
-    "rules": {
-        "vue/multi-word-component-names": "off",
-        "vue/require-default-prop": "off",
-        "vue/no-v-html": "warn",
-        "no-tabs": "off",
-        "no-mixed-spaces-and-tabs": "off"
-    }
-}

+ 31 - 0
frontend/eslint.config.mjs

@@ -0,0 +1,31 @@
+import neostandard from 'neostandard'
+import pluginVue from 'eslint-plugin-vue'
+
+export default [
+	{
+		ignores: [
+			'dist/**',
+			'node_modules/**',
+			'resources/scripts/gen/**'
+		]
+	},
+	...neostandard({
+		env: ['browser'],
+		noJsx: true
+	}),
+	...pluginVue.configs['flat/recommended'],
+	{
+		files: ['**/*.{js,mjs,vue}'],
+		languageOptions: {
+			ecmaVersion: 'latest',
+			sourceType: 'module'
+		},
+		rules: {
+			'@stylistic/no-tabs': 'off',
+			'@stylistic/no-mixed-spaces-and-tabs': 'off',
+			'vue/multi-word-component-names': 'off',
+			'vue/require-default-prop': 'off',
+			'vue/no-v-html': 'warn'
+		}
+	}
+]

Разница между файлами не показана из-за своего большого размера
+ 536 - 110
frontend/package-lock.json


+ 2 - 1
frontend/package.json

@@ -5,7 +5,9 @@
 	"repository": "https://github.com/OliveTin/OliveTin",
 	"source": "index.html",
 	"devDependencies": {
+		"eslint": "^9.39.5",
 		"eslint-plugin-vue": "^10.11.0",
+		"neostandard": "^0.13.0",
 		"process": "^0.11.10",
 		"stylelint": "^17.15.0",
 		"stylelint-config-standard": "^40.0.0"
@@ -35,7 +37,6 @@
 		"@xterm/xterm": "^6.0.0",
 		"iconify-icon": "^3.0.2",
 		"picocrank": "^1.29.0",
-		"standard": "^17.1.2",
 		"unplugin-vue-components": "^32.1.0",
 		"vite": "^8.2.2",
 		"vue": "^3.5.42",

+ 23 - 23
frontend/resources/vue/ActionButton.vue

@@ -273,9 +273,9 @@ function constructFromJson (json) {
   if (bindingId.value) {
     rateLimits[bindingId.value] = rateLimitExpires.value
     setBindingExecutionState(
-	  bindingId.value,
-	  !!json.hasRunningInstance,
-	  !!json.hasQueuedInstance
+      bindingId.value,
+      !!json.hasRunningInstance,
+      !!json.hasQueuedInstance
     )
   }
   updateRateLimitStatus()
@@ -310,8 +310,8 @@ function updateRateLimitStatus () {
     isRateLimited.value = false
     rateLimitMessage.value = ''
     if (rateLimitInterval.value) {
-	  clearInterval(rateLimitInterval.value)
-	  rateLimitInterval.value = null
+      clearInterval(rateLimitInterval.value)
+      rateLimitInterval.value = null
     }
     return
   }
@@ -325,8 +325,8 @@ function updateRateLimitStatus () {
     rateLimitMessage.value = ''
     rateLimitExpires.value = 0
     if (rateLimitInterval.value) {
-	  clearInterval(rateLimitInterval.value)
-	  rateLimitInterval.value = null
+      clearInterval(rateLimitInterval.value)
+      rateLimitInterval.value = null
     }
   } else {
     // Still rate limited
@@ -336,9 +336,9 @@ function updateRateLimitStatus () {
 
     // Set up interval to update every second
     if (!rateLimitInterval.value) {
-	  rateLimitInterval.value = setInterval(() => {
+      rateLimitInterval.value = setInterval(() => {
         updateRateLimitStatus()
-	  }, 1000)
+      }, 1000)
     }
   }
 }
@@ -360,12 +360,12 @@ async function handleClick () {
     const bindingId = props.actionData.bindingId
     const prefilled = props.prefilledArguments || {}
     if (Object.keys(prefilled).length > 0) {
-	  router.push({
+      router.push({
         path: `/actionBinding/${bindingId}/argumentForm`,
         state: { prefilledArguments: prefilled }
-	  })
+      })
     } else {
-	  router.push(`/actionBinding/${bindingId}/argumentForm`)
+      router.push(`/actionBinding/${bindingId}/argumentForm`)
     }
   } else {
     await startAction()
@@ -439,7 +439,7 @@ async function startAction (actionArgs) {
   stopButtonResultWatch = watch(
     () => buttonResults[startActionArgs.uniqueTrackingId],
     (newResult, oldResult) => {
-	  onLogEntryChanged(newResult)
+      onLogEntryChanged(newResult)
     }
   )
 
@@ -450,11 +450,11 @@ async function startAction (actionArgs) {
     const trackingId = response.executionTrackingId || startActionArgs.uniqueTrackingId
 
     if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
-	  router.push(`/logs/${trackingId}`)
+      router.push(`/logs/${trackingId}`)
     }
 
     if (!connectionState.connected) {
-	  await pollExecutionUntilDone(trackingId)
+      await pollExecutionUntilDone(trackingId)
     }
   } catch (err) {
     stopWatchingButtonResult()
@@ -554,14 +554,14 @@ onMounted(() => {
   watch(
     rateLimits,
     () => {
-	  const id = bindingId.value
-	  if (id && rateLimits[id] !== undefined) {
+      const id = bindingId.value
+      if (id && rateLimits[id] !== undefined) {
         const newExpires = rateLimits[id]
         if (newExpires !== rateLimitExpires.value) {
-		  rateLimitExpires.value = newExpires
-		  updateRateLimitStatus()
+          rateLimitExpires.value = newExpires
+          updateRateLimitStatus()
         }
-	  }
+      }
     },
     { deep: true }
   )
@@ -570,9 +570,9 @@ onMounted(() => {
   watch(
     () => pendingBindingFlash[bindingId.value],
     (pending) => {
-	  if (pending) {
+      if (pending) {
         consumeAndFlashPendingResult()
-	  }
+      }
     },
     { immediate: true }
   )
@@ -593,7 +593,7 @@ watch(
   (newData) => {
     updateFromJson(newData)
     if (newData?.icon !== undefined) {
-	  glyph.value = newData.icon ?? ''
+      glyph.value = newData.icon ?? ''
     }
   },
   { deep: true }

+ 29 - 29
frontend/resources/vue/components/actionIconGlyphHelpers.mjs

@@ -1,38 +1,38 @@
 const fallbackNamedHtmlEntities = {
-	amp: '&',
-	apos: "'",
-	darr: '\u2193',
-	gt: '>',
-	laquo: '\u00ab',
-	larr: '\u2190',
-	nbsp: '\u00a0',
-	quot: '"',
-	raquo: '\u00bb',
-	rarr: '\u2192',
-	uarr: '\u2191',
+  amp: '&',
+  apos: "'",
+  darr: '\u2193',
+  gt: '>',
+  laquo: '\u00ab',
+  larr: '\u2190',
+  nbsp: '\u00a0',
+  quot: '"',
+  raquo: '\u00bb',
+  rarr: '\u2192',
+  uarr: '\u2191',
 }
 
-export function decodeHtmlEntities(text) {
-	if (typeof document !== 'undefined') {
-		const textarea = document.createElement('textarea')
-		textarea.innerHTML = text
+export function decodeHtmlEntities (text) {
+  if (typeof document !== 'undefined') {
+    const textarea = document.createElement('textarea')
+    textarea.innerHTML = text
 
-		return textarea.value
-	}
+    return textarea.value
+  }
 
-	return text.replace(/&#x([0-9a-fA-F]+);?/g, (_, hex) => {
-		const codePoint = Number.parseInt(hex, 16)
-		return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : ''
-	}).replace(/&#(\d+);?/g, (_, decimal) => {
-		const codePoint = Number.parseInt(decimal, 10)
-		return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : ''
-	}).replace(/&([a-zA-Z][a-zA-Z0-9]+);?/g, (entity, name) => {
-		return fallbackNamedHtmlEntities[name] ?? entity
-	})
+  return text.replace(/&#x([0-9a-fA-F]+);?/g, (_, hex) => {
+    const codePoint = Number.parseInt(hex, 16)
+    return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : ''
+  }).replace(/&#(\d+);?/g, (_, decimal) => {
+    const codePoint = Number.parseInt(decimal, 10)
+    return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : ''
+  }).replace(/&([a-zA-Z][a-zA-Z0-9]+);?/g, (entity, name) => {
+    return fallbackNamedHtmlEntities[name] ?? entity
+  })
 }
 
-export function glyphLooksLikeHtml(text) {
-	const trimmedText = text.trim()
+export function glyphLooksLikeHtml (text) {
+  const trimmedText = text.trim()
 
-	return trimmedText.startsWith('<') || /<img\b/i.test(text) || /\/custom-webui\//i.test(text)
+  return trimmedText.startsWith('<') || /<img\b/i.test(text) || /\/custom-webui\//i.test(text)
 }

+ 7 - 7
frontend/resources/vue/components/actionIconGlyphHelpers.test.mjs

@@ -3,18 +3,18 @@ import assert from 'node:assert/strict'
 import { decodeHtmlEntities, glyphLooksLikeHtml } from './actionIconGlyphHelpers.mjs'
 
 test('decodeHtmlEntities decodes named entity icons as plain glyph text', () => {
-	assert.equal(decodeHtmlEntities('&laquo;'), '\u00ab')
-	assert.equal(decodeHtmlEntities('&rarr;'), '\u2192')
-	assert.equal(decodeHtmlEntities('&laquo; next &rarr;'), '\u00ab next \u2192')
+  assert.equal(decodeHtmlEntities('&laquo;'), '\u00ab')
+  assert.equal(decodeHtmlEntities('&rarr;'), '\u2192')
+  assert.equal(decodeHtmlEntities('&laquo; next &rarr;'), '\u00ab next \u2192')
 })
 
 test('decoded named entity icons are not treated as HTML markup', () => {
-	const decodedGlyph = decodeHtmlEntities('&rarr;')
+  const decodedGlyph = decodeHtmlEntities('&rarr;')
 
-	assert.equal(glyphLooksLikeHtml(decodedGlyph), false)
+  assert.equal(glyphLooksLikeHtml(decodedGlyph), false)
 })
 
 test('decodeHtmlEntities keeps existing numeric entity icon support', () => {
-	assert.equal(decodeHtmlEntities('&#x1f4a9;'), '\ud83d\udca9')
-	assert.equal(decodeHtmlEntities('&#128190;'), '\ud83d\udcbe')
+  assert.equal(decodeHtmlEntities('&#x1f4a9;'), '\ud83d\udca9')
+  assert.equal(decodeHtmlEntities('&#128190;'), '\ud83d\udcbe')
 })

+ 1 - 1
frontend/resources/vue/utils/choiceChecklistHelpers.test.mjs

@@ -13,7 +13,7 @@ const choices = [
   { title: 'Photos', value: 'photos' }
 ]
 
-	test('parseChecklistValue parses JSON-encoded values', () => {
+test('parseChecklistValue parses JSON-encoded values', () => {
   assert.deepEqual(parseChecklistValue('["documents","photos"]'), ['documents', 'photos'])
   assert.deepEqual(parseChecklistValue('["kitchen,bedroom","hallway"]'), ['kitchen,bedroom', 'hallway'])
   assert.deepEqual(parseChecklistValue(''), [])

+ 18 - 18
frontend/resources/vue/utils/prefilledArguments.test.mjs

@@ -4,31 +4,31 @@ import assert from 'node:assert/strict'
 import { getInitialArgumentValue, readPrefilledArgumentsFromNavigation } from './prefilledArguments.js'
 
 test('readPrefilledArgumentsFromNavigation returns navigation state values', () => {
-	assert.deepEqual(
-		readPrefilledArgumentsFromNavigation({ prefilledArguments: { ansible_host: '10.0.0.1' } }),
-		{ ansible_host: '10.0.0.1' }
-	)
+  assert.deepEqual(
+    readPrefilledArgumentsFromNavigation({ prefilledArguments: { ansible_host: '10.0.0.1' } }),
+    { ansible_host: '10.0.0.1' }
+  )
 })
 
 test('readPrefilledArgumentsFromNavigation returns empty object when state is absent', () => {
-	assert.deepEqual(readPrefilledArgumentsFromNavigation({}), {})
-	assert.deepEqual(readPrefilledArgumentsFromNavigation(undefined), {})
+  assert.deepEqual(readPrefilledArgumentsFromNavigation({}), {})
+  assert.deepEqual(readPrefilledArgumentsFromNavigation(undefined), {})
 })
 
 test('getInitialArgumentValue prefers navigation state over query params', () => {
-	assert.equal(
-		getInitialArgumentValue(
-			'ansible_host',
-			{ ansible_host: '10.0.0.1' },
-			'?ansible_host=10.0.0.2'
-		),
-		'10.0.0.1'
-	)
+  assert.equal(
+    getInitialArgumentValue(
+      'ansible_host',
+      { ansible_host: '10.0.0.1' },
+      '?ansible_host=10.0.0.2'
+    ),
+    '10.0.0.1'
+  )
 })
 
 test('getInitialArgumentValue falls back to query params when state is absent', () => {
-	assert.equal(
-		getInitialArgumentValue('ansible_host', {}, '?ansible_host=10.0.0.2'),
-		'10.0.0.2'
-	)
+  assert.equal(
+    getInitialArgumentValue('ansible_host', {}, '?ansible_host=10.0.0.2'),
+    '10.0.0.2'
+  )
 })

+ 7 - 7
frontend/resources/vue/views/ExecutionView.vue

@@ -355,10 +355,10 @@ async function fetchExecutionResult (executionTrackingIdParam) {
   } catch (err) {
     // Check if it's a "not found" error (404 or similar)
     if (err.status === 404 || err.code === 'NotFound' || err.message?.includes('not found')) {
-	  notFound.value = true
-	  errorMessage.value = err.message || 'The execution could not be found in the system.'
+      notFound.value = true
+      errorMessage.value = err.message || 'The execution could not be found in the system.'
     } else {
-	  renderError(err)
+      renderError(err)
     }
     throw err
   }
@@ -434,7 +434,7 @@ async function renderExecutionResult (res) {
   if (terminal) {
     await terminal.reset()
     await terminal.write(res.logEntry.output, () => {
-	  terminal.fit()
+      terminal.fit()
     })
   }
 }
@@ -488,11 +488,11 @@ onMounted(() => {
   watch(
     () => buttonResults[props.executionTrackingId],
     (newResult, oldResult) => {
-	  if (newResult) {
+      if (newResult) {
         renderExecutionResult({
-		  logEntry: newResult
+          logEntry: newResult
         })
-	  }
+      }
     }
   )
 })

Некоторые файлы не были показаны из-за большого количества измененных файлов