generated from Nodarx/template
chore: implement js test suites
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
name: Build Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Retrieve Server Install Action
|
||||||
|
uses: actions/checkout@v6.0.2
|
||||||
|
with:
|
||||||
|
repository: Nodarx/action-server-install
|
||||||
|
ref: main
|
||||||
|
path: action-server-install
|
||||||
|
github-server-url: https://git.ktrix.dev
|
||||||
|
|
||||||
|
- name: Install Server
|
||||||
|
uses: ./action-server-install
|
||||||
|
with:
|
||||||
|
install-php: 'false'
|
||||||
|
install-node: 'true'
|
||||||
|
php-version: '8.5'
|
||||||
|
node-version: '24'
|
||||||
|
server-path: './server'
|
||||||
|
|
||||||
|
- name: Checkout Pull Request
|
||||||
|
uses: actions/checkout@v6.0.2
|
||||||
|
with:
|
||||||
|
repository: ${{ github.repository }}
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
path: server/modules/documents_text_editor
|
||||||
|
github-server-url: https://git.ktrix.dev
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
working-directory: server/modules/documents_text_editor
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
working-directory: server/modules/documents_text_editor
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
name: JS Unit Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Retrieve Server Install Action
|
||||||
|
uses: actions/checkout@v6.0.2
|
||||||
|
with:
|
||||||
|
repository: Nodarx/action-server-install
|
||||||
|
ref: main
|
||||||
|
path: action-server-install
|
||||||
|
github-server-url: https://git.ktrix.dev
|
||||||
|
|
||||||
|
- name: Install Server
|
||||||
|
uses: ./action-server-install
|
||||||
|
with:
|
||||||
|
install-php: 'false'
|
||||||
|
install-node: 'true'
|
||||||
|
php-version: '8.5'
|
||||||
|
node-version: '24'
|
||||||
|
server-path: './server'
|
||||||
|
|
||||||
|
- name: Checkout Pull Request
|
||||||
|
uses: actions/checkout@v6.0.2
|
||||||
|
with:
|
||||||
|
repository: ${{ github.repository }}
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
path: server/modules/documents_text_editor
|
||||||
|
github-server-url: https://git.ktrix.dev
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
working-directory: server/modules/documents_text_editor
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: npm run test:unit
|
||||||
|
working-directory: server/modules/documents_text_editor
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
name: Renovate
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 3 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
renovate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6.0.2
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v6.2.0
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Set up PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.5'
|
||||||
|
tools: composer:v2
|
||||||
|
|
||||||
|
- name: Install Renovate
|
||||||
|
run: |
|
||||||
|
npm install --global --no-audit --fund=false \
|
||||||
|
--prefix "${{ runner.temp }}/renovate-npm" \
|
||||||
|
--cache "${{ runner.temp }}/renovate-npm-cache" \
|
||||||
|
renovate
|
||||||
|
"${{ runner.temp }}/renovate-npm/bin/renovate" --version
|
||||||
|
|
||||||
|
- name: Run Renovate
|
||||||
|
env:
|
||||||
|
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||||
|
RENOVATE_PLATFORM: gitea
|
||||||
|
RENOVATE_ENDPOINT: https://git.ktrix.dev/api/v1
|
||||||
|
run: |
|
||||||
|
"${{ runner.temp }}/renovate-npm/bin/renovate" ${{ gitea.repository }}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
# Frontend development
|
||||||
|
node_modules/
|
||||||
|
*.local
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
.cache/
|
||||||
|
.vite/
|
||||||
|
.temp/
|
||||||
|
.tmp/
|
||||||
|
|
||||||
|
# Frontend build
|
||||||
|
/static/
|
||||||
|
|
||||||
|
# Backend development
|
||||||
|
/vendor/
|
||||||
|
/lib/vendor/
|
||||||
|
coverage/
|
||||||
|
*.cache
|
||||||
|
.phpactor/
|
||||||
|
|
||||||
|
# Editors
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace KTXM\DocumentsTextEditor;
|
||||||
|
|
||||||
|
use KTXF\Module\ModuleBrowserInterface;
|
||||||
|
use KTXF\Module\ModuleInstanceAbstract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Documents Text Editor — provides inline editing for text, Markdown, and HTML files
|
||||||
|
* via the documents_file_editor integration point.
|
||||||
|
*/
|
||||||
|
class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
|
||||||
|
{
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public function handle(): string
|
||||||
|
{
|
||||||
|
return 'documents_text_editor';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return 'Documents Text Editor';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function author(): string
|
||||||
|
{
|
||||||
|
return 'Ktrix';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Provides inline text, Markdown, and HTML editing for the Documents module via the file editor integration point.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function version(): string
|
||||||
|
{
|
||||||
|
return '0.0.1';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function permissions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerBI(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'handle' => $this->handle(),
|
||||||
|
'namespace' => 'DocumentsTextEditor',
|
||||||
|
'version' => $this->version(),
|
||||||
|
'label' => $this->label(),
|
||||||
|
'author' => $this->author(),
|
||||||
|
'description' => $this->description(),
|
||||||
|
'boot' => 'static/module.mjs',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+4203
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "documents_text_editor",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"author": "Ktrix",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build --mode production --config vite.config.ts",
|
||||||
|
"dev": "vite build --mode development --config vite.config.ts",
|
||||||
|
"watch": "vite build --mode development --watch --config vite.config.ts",
|
||||||
|
"typecheck": "vue-tsc --noEmit",
|
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||||
|
"test": "vitest run --config tests/js/vitest.config.ts",
|
||||||
|
"test:unit": "vitest run --config tests/js/vitest.config.ts",
|
||||||
|
"test:watch": "vitest watch --config tests/js/vitest.config.ts",
|
||||||
|
"test:coverage": "vitest run --coverage --config tests/js/vitest.config.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tiptap/vue-3": "^2.11.5",
|
||||||
|
"@tiptap/pm": "^2.11.5",
|
||||||
|
"@tiptap/starter-kit": "^2.11.5",
|
||||||
|
"@tiptap/extension-link": "^2.11.5",
|
||||||
|
"@tiptap/extension-underline": "^2.11.5",
|
||||||
|
"@tiptap/extension-placeholder": "^2.11.5",
|
||||||
|
"@tiptap/extension-text-align": "^2.11.5",
|
||||||
|
"tiptap-markdown": "^0.8.10",
|
||||||
|
"pinia": "^2.3.1",
|
||||||
|
"vue": "^3.5.18",
|
||||||
|
"vue-router": "^4.5.1",
|
||||||
|
"vuetify": "^3.10.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"vite": "^7.1.2",
|
||||||
|
"vue-tsc": "^3.0.5",
|
||||||
|
"@vitest/coverage-v8": "^4.1.6",
|
||||||
|
"@vue/test-utils": "^2.4.10",
|
||||||
|
"jsdom": "^29.1.1",
|
||||||
|
"vitest": "^4.1.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": ["config:recommended"],
|
||||||
|
"enabledManagers": ["npm", "composer", "github-actions"],
|
||||||
|
"timezone": "UTC",
|
||||||
|
"schedule": ["* 0-3 * * *"],
|
||||||
|
"dependencyDashboard": true,
|
||||||
|
"prConcurrentLimit": 5,
|
||||||
|
"prHourlyLimit": 2
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import type { Editor } from '@tiptap/vue-3'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
editor: Editor | null
|
||||||
|
isMarkdown: boolean
|
||||||
|
sourceMode: boolean
|
||||||
|
isDirty: boolean
|
||||||
|
isSaving: boolean
|
||||||
|
showRichControls: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
save: []
|
||||||
|
'toggle-mode': [toSource: boolean]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Link dialog state
|
||||||
|
const showLinkDialog = ref(false)
|
||||||
|
const linkUrl = ref('')
|
||||||
|
|
||||||
|
function openLinkDialog(editor: Editor) {
|
||||||
|
const existing = editor.getAttributes('link').href as string | undefined
|
||||||
|
linkUrl.value = existing ?? ''
|
||||||
|
showLinkDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLink(editor: Editor) {
|
||||||
|
showLinkDialog.value = false
|
||||||
|
const url = linkUrl.value.trim()
|
||||||
|
if (url) {
|
||||||
|
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
|
||||||
|
} else {
|
||||||
|
editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||||
|
}
|
||||||
|
linkUrl.value = ''
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="editor-toolbar-strip">
|
||||||
|
<!-- Rich text formatting controls -->
|
||||||
|
<template v-if="showRichControls && editor">
|
||||||
|
<v-btn-group density="compact" variant="text">
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
icon="mdi-format-bold"
|
||||||
|
:active="editor.isActive('bold')"
|
||||||
|
@click="editor.chain().focus().toggleBold().run()"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
icon="mdi-format-italic"
|
||||||
|
:active="editor.isActive('italic')"
|
||||||
|
@click="editor.chain().focus().toggleItalic().run()"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
icon="mdi-format-underline"
|
||||||
|
:active="editor.isActive('underline')"
|
||||||
|
@click="editor.chain().focus().toggleUnderline().run()"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
icon="mdi-format-strikethrough"
|
||||||
|
:active="editor.isActive('strike')"
|
||||||
|
@click="editor.chain().focus().toggleStrike().run()"
|
||||||
|
/>
|
||||||
|
</v-btn-group>
|
||||||
|
|
||||||
|
<v-divider vertical class="mx-1" />
|
||||||
|
|
||||||
|
<v-btn-group density="compact" variant="text">
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
icon="mdi-format-header-1"
|
||||||
|
:active="editor.isActive('heading', { level: 1 })"
|
||||||
|
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
icon="mdi-format-header-2"
|
||||||
|
:active="editor.isActive('heading', { level: 2 })"
|
||||||
|
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
icon="mdi-format-header-3"
|
||||||
|
:active="editor.isActive('heading', { level: 3 })"
|
||||||
|
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
|
||||||
|
/>
|
||||||
|
</v-btn-group>
|
||||||
|
|
||||||
|
<v-divider vertical class="mx-1" />
|
||||||
|
|
||||||
|
<v-btn-group density="compact" variant="text">
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
icon="mdi-format-list-bulleted"
|
||||||
|
:active="editor.isActive('bulletList')"
|
||||||
|
@click="editor.chain().focus().toggleBulletList().run()"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
icon="mdi-format-list-numbered"
|
||||||
|
:active="editor.isActive('orderedList')"
|
||||||
|
@click="editor.chain().focus().toggleOrderedList().run()"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
icon="mdi-format-quote-close"
|
||||||
|
:active="editor.isActive('blockquote')"
|
||||||
|
@click="editor.chain().focus().toggleBlockquote().run()"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
icon="mdi-code-tags"
|
||||||
|
:active="editor.isActive('codeBlock')"
|
||||||
|
@click="editor.chain().focus().toggleCodeBlock().run()"
|
||||||
|
/>
|
||||||
|
</v-btn-group>
|
||||||
|
|
||||||
|
<v-divider vertical class="mx-1" />
|
||||||
|
|
||||||
|
<v-btn-group density="compact" variant="text">
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
icon="mdi-link"
|
||||||
|
:active="editor.isActive('link')"
|
||||||
|
@click="openLinkDialog(editor)"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
icon="mdi-minus"
|
||||||
|
@click="editor.chain().focus().setHorizontalRule().run()"
|
||||||
|
/>
|
||||||
|
</v-btn-group>
|
||||||
|
|
||||||
|
<v-divider vertical class="mx-1" />
|
||||||
|
|
||||||
|
<v-btn-group density="compact" variant="text">
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
icon="mdi-undo"
|
||||||
|
:disabled="!editor.can().undo()"
|
||||||
|
@click="editor.chain().focus().undo().run()"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
icon="mdi-redo"
|
||||||
|
:disabled="!editor.can().redo()"
|
||||||
|
@click="editor.chain().focus().redo().run()"
|
||||||
|
/>
|
||||||
|
</v-btn-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-spacer />
|
||||||
|
|
||||||
|
<!-- Mode toggle for markdown -->
|
||||||
|
<v-btn-group v-if="isMarkdown" density="compact" variant="text" class="mr-2">
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
:active="!sourceMode"
|
||||||
|
@click="emit('toggle-mode', false)"
|
||||||
|
>
|
||||||
|
<v-icon start size="small">mdi-eye</v-icon>
|
||||||
|
Visual
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
:active="sourceMode"
|
||||||
|
@click="emit('toggle-mode', true)"
|
||||||
|
>
|
||||||
|
<v-icon start size="small">mdi-code-tags</v-icon>
|
||||||
|
Source
|
||||||
|
</v-btn>
|
||||||
|
</v-btn-group>
|
||||||
|
|
||||||
|
<!-- Save button -->
|
||||||
|
<v-btn
|
||||||
|
variant="tonal"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
:disabled="!isDirty || isSaving"
|
||||||
|
:loading="isSaving"
|
||||||
|
@click="emit('save')"
|
||||||
|
>
|
||||||
|
<v-icon start size="small">mdi-content-save</v-icon>
|
||||||
|
Save
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Link dialog -->
|
||||||
|
<v-dialog v-model="showLinkDialog" max-width="400">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-body-1">Insert Link</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-text-field
|
||||||
|
v-model="linkUrl"
|
||||||
|
label="URL"
|
||||||
|
placeholder="https://example.com"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
autofocus
|
||||||
|
@keydown.enter="editor && applyLink(editor)"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="showLinkDialog = false">Cancel</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="tonal"
|
||||||
|
:disabled="!editor"
|
||||||
|
@click="editor && applyLink(editor)"
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.editor-toolbar-strip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||||
|
min-height: 44px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onBeforeUnmount, watch } from 'vue'
|
||||||
|
import { useEditor, EditorContent, type Editor } from '@tiptap/vue-3'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import Link from '@tiptap/extension-link'
|
||||||
|
import Underline from '@tiptap/extension-underline'
|
||||||
|
import TextAlign from '@tiptap/extension-text-align'
|
||||||
|
import Placeholder from '@tiptap/extension-placeholder'
|
||||||
|
import { Markdown } from 'tiptap-markdown'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
mime: string
|
||||||
|
fileExtension: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
update: []
|
||||||
|
'editor-ready': [editor: Editor]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isMarkdown = props.mime === 'text/markdown' || props.mime === 'text/x-markdown' ||
|
||||||
|
props.fileExtension === 'md' || props.fileExtension === 'markdown'
|
||||||
|
|
||||||
|
const editor = useEditor({
|
||||||
|
onCreate: ({ editor: e }) => {
|
||||||
|
emit('editor-ready', e)
|
||||||
|
},
|
||||||
|
extensions: [
|
||||||
|
StarterKit.configure({
|
||||||
|
heading: { levels: [1, 2, 3, 4, 5, 6] },
|
||||||
|
}),
|
||||||
|
Link.configure({
|
||||||
|
openOnClick: false,
|
||||||
|
HTMLAttributes: { rel: 'noopener noreferrer nofollow' },
|
||||||
|
}),
|
||||||
|
Underline,
|
||||||
|
TextAlign.configure({
|
||||||
|
types: ['heading', 'paragraph'],
|
||||||
|
}),
|
||||||
|
Placeholder.configure({
|
||||||
|
placeholder: 'Start typing…',
|
||||||
|
}),
|
||||||
|
...(isMarkdown
|
||||||
|
? [
|
||||||
|
Markdown.configure({
|
||||||
|
html: false,
|
||||||
|
transformPastedText: true,
|
||||||
|
transformCopiedText: true,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
onUpdate: () => {
|
||||||
|
emit('update')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function setContent(content: string) {
|
||||||
|
if (!editor.value) return
|
||||||
|
editor.value.commands.setContent(content, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContent(): string {
|
||||||
|
if (!editor.value) return ''
|
||||||
|
if (isMarkdown) {
|
||||||
|
return editor.value.storage.markdown.getMarkdown()
|
||||||
|
}
|
||||||
|
return editor.value.getHTML()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
editor.value?.destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for editor recreation (shouldn't happen, but safety net)
|
||||||
|
watch(editor, (newEditor, oldEditor) => {
|
||||||
|
if (oldEditor && oldEditor !== newEditor) {
|
||||||
|
oldEditor.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
editor,
|
||||||
|
setContent,
|
||||||
|
getContent,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rich-text-editor">
|
||||||
|
<EditorContent v-if="editor" :editor="editor" class="rich-text-content" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rich-text-editor {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-content :deep(.tiptap) {
|
||||||
|
padding: 16px 24px;
|
||||||
|
outline: none;
|
||||||
|
min-height: 100%;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-content :deep(.tiptap p.is-editor-empty:first-child::before) {
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.4);
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
float: left;
|
||||||
|
height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Headings */
|
||||||
|
.rich-text-content :deep(.tiptap h1) { font-size: 2rem; margin: 1em 0 0.5em; }
|
||||||
|
.rich-text-content :deep(.tiptap h2) { font-size: 1.5rem; margin: 1em 0 0.5em; }
|
||||||
|
.rich-text-content :deep(.tiptap h3) { font-size: 1.25rem; margin: 1em 0 0.5em; }
|
||||||
|
|
||||||
|
/* Blockquote */
|
||||||
|
.rich-text-content :deep(.tiptap blockquote) {
|
||||||
|
border-left: 3px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||||
|
padding-left: 16px;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks */
|
||||||
|
.rich-text-content :deep(.tiptap pre) {
|
||||||
|
background: rgba(var(--v-theme-on-surface), 0.05);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-content :deep(.tiptap code) {
|
||||||
|
background: rgba(var(--v-theme-on-surface), 0.08);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal rule */
|
||||||
|
.rich-text-content :deep(.tiptap hr) {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links */
|
||||||
|
.rich-text-content :deep(.tiptap a) {
|
||||||
|
color: rgb(var(--v-theme-primary));
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lists */
|
||||||
|
.rich-text-content :deep(.tiptap ul),
|
||||||
|
.rich-text-content :deep(.tiptap ol) {
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
update: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const content = ref('')
|
||||||
|
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
||||||
|
|
||||||
|
function setContent(value: string) {
|
||||||
|
content.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContent(): string {
|
||||||
|
return content.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInput() {
|
||||||
|
emit('update')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
// Tab inserts 2 spaces instead of changing focus
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
e.preventDefault()
|
||||||
|
const textarea = textareaRef.value
|
||||||
|
if (!textarea) return
|
||||||
|
|
||||||
|
const start = textarea.selectionStart
|
||||||
|
const end = textarea.selectionEnd
|
||||||
|
const indent = ' '
|
||||||
|
|
||||||
|
content.value =
|
||||||
|
content.value.substring(0, start) + indent + content.value.substring(end)
|
||||||
|
|
||||||
|
// Restore cursor position
|
||||||
|
nextTick(() => {
|
||||||
|
textarea.selectionStart = textarea.selectionEnd = start + indent.length
|
||||||
|
})
|
||||||
|
|
||||||
|
emit('update')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
textareaRef.value?.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
setContent,
|
||||||
|
getContent,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="source-editor">
|
||||||
|
<textarea
|
||||||
|
ref="textareaRef"
|
||||||
|
v-model="content"
|
||||||
|
class="source-textarea"
|
||||||
|
spellcheck="false"
|
||||||
|
autocomplete="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
@input="handleInput"
|
||||||
|
@keydown="handleKeydown"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.source-editor {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-textarea {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
background: transparent;
|
||||||
|
color: rgb(var(--v-theme-on-surface));
|
||||||
|
font-family: 'Roboto Mono', 'Courier New', monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
tab-size: 2;
|
||||||
|
white-space: pre;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, onUnmounted, ref, shallowRef, watch, nextTick } from 'vue'
|
||||||
|
import type { EntityObject } from '@DocumentsManager/models/entity'
|
||||||
|
|
||||||
|
import RichTextEditor from './RichTextEditor.vue'
|
||||||
|
import SourceEditor from './SourceEditor.vue'
|
||||||
|
import EditorToolbar from './EditorToolbar.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
entity: EntityObject
|
||||||
|
mime: string
|
||||||
|
readFile: (entityId: string) => Promise<string | null>
|
||||||
|
writeFile: (entityId: string, content: string) => Promise<number>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
dirty: [value: boolean]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// --- State ---
|
||||||
|
const loading = ref(true)
|
||||||
|
const saving = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
const isDirty = ref(false)
|
||||||
|
const lastSavedContent = ref('')
|
||||||
|
|
||||||
|
// For markdown: toggle between visual (rich) and source mode
|
||||||
|
const sourceMode = ref(false)
|
||||||
|
|
||||||
|
// References to child editors
|
||||||
|
const richEditorRef = ref<InstanceType<typeof RichTextEditor> | null>(null)
|
||||||
|
const sourceEditorRef = ref<InstanceType<typeof SourceEditor> | null>(null)
|
||||||
|
|
||||||
|
// TipTap editor instance, set when RichTextEditor emits it
|
||||||
|
const tiptapEditor = shallowRef<any>(null)
|
||||||
|
|
||||||
|
// --- Computed ---
|
||||||
|
// Detect markdown by MIME type or file extension (servers often report .md as text/plain)
|
||||||
|
const fileExtension = computed(() => {
|
||||||
|
const label = props.entity?.properties?.label ?? ''
|
||||||
|
const dot = label.lastIndexOf('.')
|
||||||
|
return dot >= 0 ? label.substring(dot + 1).toLowerCase() : ''
|
||||||
|
})
|
||||||
|
const isMarkdown = computed(() =>
|
||||||
|
props.mime === 'text/markdown' || props.mime === 'text/x-markdown' ||
|
||||||
|
fileExtension.value === 'md' || fileExtension.value === 'markdown',
|
||||||
|
)
|
||||||
|
const isHtml = computed(() => props.mime === 'text/html' || fileExtension.value === 'html' || fileExtension.value === 'htm')
|
||||||
|
const isPlainText = computed(() => !isMarkdown.value && !isHtml.value)
|
||||||
|
|
||||||
|
// Show rich editor for HTML always, and for markdown when not in source mode
|
||||||
|
const showRichEditor = computed(() => isHtml.value || (isMarkdown.value && !sourceMode.value))
|
||||||
|
const showSourceEditor = computed(() => isPlainText.value || (isMarkdown.value && sourceMode.value))
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
function decodeBase64(base64: string): string {
|
||||||
|
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0))
|
||||||
|
return new TextDecoder().decode(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeBase64(text: string): string {
|
||||||
|
const bytes = new TextEncoder().encode(text)
|
||||||
|
let binary = ''
|
||||||
|
for (const byte of bytes) {
|
||||||
|
binary += String.fromCharCode(byte)
|
||||||
|
}
|
||||||
|
return btoa(binary)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Content loading ---
|
||||||
|
async function loadContent() {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
const raw = await props.readFile(String(props.entity.identifier))
|
||||||
|
const decoded = raw ? decodeBase64(raw) : ''
|
||||||
|
lastSavedContent.value = decoded
|
||||||
|
isDirty.value = false
|
||||||
|
emit('dirty', false)
|
||||||
|
// Set loading false first so editors render, then set content after they mount
|
||||||
|
loading.value = false
|
||||||
|
await nextTick()
|
||||||
|
setEditorContent(decoded)
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e instanceof Error ? e.message : 'Failed to load file'
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEditorContent(content: string) {
|
||||||
|
if (showRichEditor.value && richEditorRef.value) {
|
||||||
|
richEditorRef.value.setContent(content)
|
||||||
|
} else if (showSourceEditor.value && sourceEditorRef.value) {
|
||||||
|
sourceEditorRef.value.setContent(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEditorContent(): string {
|
||||||
|
if (showRichEditor.value && richEditorRef.value) {
|
||||||
|
return richEditorRef.value.getContent()
|
||||||
|
}
|
||||||
|
if (showSourceEditor.value && sourceEditorRef.value) {
|
||||||
|
return sourceEditorRef.value.getContent()
|
||||||
|
}
|
||||||
|
return lastSavedContent.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Save ---
|
||||||
|
async function handleSave() {
|
||||||
|
if (!isDirty.value || saving.value) return
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const content = getEditorContent()
|
||||||
|
const encoded = encodeBase64(content)
|
||||||
|
await props.writeFile(String(props.entity.identifier), encoded)
|
||||||
|
lastSavedContent.value = content
|
||||||
|
isDirty.value = false
|
||||||
|
emit('dirty', false)
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e instanceof Error ? e.message : 'Failed to save file'
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Dirty tracking ---
|
||||||
|
function handleContentChange() {
|
||||||
|
const current = getEditorContent()
|
||||||
|
const dirty = current !== lastSavedContent.value
|
||||||
|
if (dirty !== isDirty.value) {
|
||||||
|
isDirty.value = dirty
|
||||||
|
emit('dirty', dirty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mode switch (markdown only) ---
|
||||||
|
function handleModeToggle(toSource: boolean) {
|
||||||
|
// Serialize from current editor before switching
|
||||||
|
const content = getEditorContent()
|
||||||
|
sourceMode.value = toSource
|
||||||
|
tiptapEditor.value = null
|
||||||
|
nextTick(() => {
|
||||||
|
setEditorContent(content)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Keyboard shortcut: Ctrl/Cmd+S ---
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||||
|
e.preventDefault()
|
||||||
|
handleSave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('keydown', handleKeydown)
|
||||||
|
loadContent()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('keydown', handleKeydown)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reload when entity changes
|
||||||
|
watch(
|
||||||
|
() => props.entity.identifier,
|
||||||
|
() => {
|
||||||
|
sourceMode.value = false
|
||||||
|
loadContent()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="text-editor-viewer">
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<EditorToolbar
|
||||||
|
:editor="tiptapEditor"
|
||||||
|
:is-markdown="isMarkdown"
|
||||||
|
:source-mode="sourceMode"
|
||||||
|
:is-dirty="isDirty"
|
||||||
|
:is-saving="saving"
|
||||||
|
:show-rich-controls="showRichEditor"
|
||||||
|
@save="handleSave"
|
||||||
|
@toggle-mode="handleModeToggle"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Loading -->
|
||||||
|
<div v-if="loading" class="editor-state-overlay">
|
||||||
|
<v-progress-circular indeterminate color="primary" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error -->
|
||||||
|
<div v-else-if="error" class="editor-state-overlay">
|
||||||
|
<v-icon size="48" color="error">mdi-alert-circle-outline</v-icon>
|
||||||
|
<p class="text-body-1 mt-3 text-error">{{ error }}</p>
|
||||||
|
<v-btn variant="tonal" class="mt-3" @click="loadContent">Retry</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editor -->
|
||||||
|
<template v-else>
|
||||||
|
<RichTextEditor
|
||||||
|
v-if="showRichEditor"
|
||||||
|
ref="richEditorRef"
|
||||||
|
:mime="mime"
|
||||||
|
:file-extension="fileExtension"
|
||||||
|
@update="handleContentChange"
|
||||||
|
@editor-ready="tiptapEditor = $event"
|
||||||
|
/>
|
||||||
|
<SourceEditor
|
||||||
|
v-if="showSourceEditor"
|
||||||
|
ref="sourceEditorRef"
|
||||||
|
@update="handleContentChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.text-editor-viewer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-state-overlay {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export { default as TextEditorViewer } from './TextEditorViewer.vue'
|
||||||
|
export { default as RichTextEditor } from './RichTextEditor.vue'
|
||||||
|
export { default as SourceEditor } from './SourceEditor.vue'
|
||||||
|
export { default as EditorToolbar } from './EditorToolbar.vue'
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { ModuleIntegrations } from '@KTXC/types/moduleTypes'
|
||||||
|
|
||||||
|
const integrations: ModuleIntegrations = {
|
||||||
|
/**
|
||||||
|
* Register the text editor for text/plain, text/markdown, and text/html MIME types.
|
||||||
|
*
|
||||||
|
* Editor component contract:
|
||||||
|
* props: { entity: EntityObject, mime: string, readFile: Function, writeFile: Function }
|
||||||
|
*/
|
||||||
|
documents_file_editor: [
|
||||||
|
{
|
||||||
|
id: 'text_editor',
|
||||||
|
label: 'Text Editor',
|
||||||
|
priority: 20,
|
||||||
|
meta: {
|
||||||
|
mimeTypes: ['text/plain', 'text/markdown', 'text/x-markdown', 'text/html'],
|
||||||
|
},
|
||||||
|
component: () => import('./components/TextEditorViewer.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default integrations
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import integrations from '@/integrations'
|
||||||
|
|
||||||
|
// CSS filename is injected by the vite plugin at build time
|
||||||
|
// The placeholder gets replaced with the actual hashed filename
|
||||||
|
export const css = ['__CSS_FILENAME_PLACEHOLDER__']
|
||||||
|
|
||||||
|
export { integrations }
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
|
||||||
|
describe('Basic Tests', () => {
|
||||||
|
it('should perform basic assertion', () => {
|
||||||
|
expect(true).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should test array operations', () => {
|
||||||
|
const array = ['foo', 'bar', 'baz']
|
||||||
|
|
||||||
|
expect(array).toHaveLength(3)
|
||||||
|
expect(array).toContain('bar')
|
||||||
|
expect(array[0]).toBe('foo')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should test string operations', () => {
|
||||||
|
const string = 'Hello, World!'
|
||||||
|
|
||||||
|
expect(string).toContain('World')
|
||||||
|
expect(string.length).toBe(13)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should test object operations', () => {
|
||||||
|
const obj = { foo: 'bar', count: 42 }
|
||||||
|
|
||||||
|
expect(obj).toHaveProperty('foo')
|
||||||
|
expect(obj.foo).toBe('bar')
|
||||||
|
expect(obj.count).toBeGreaterThan(40)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { defineConfig, configDefaults } from 'vitest/config'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = path.dirname(__filename)
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, '../../src'),
|
||||||
|
'@KTXC': path.resolve(__dirname, '../../../../core/src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
exclude: [...configDefaults.exclude, 'e2e/**'],
|
||||||
|
root: fileURLToPath(new URL('../../', import.meta.url)),
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
reporter: ['text', 'json', 'html'],
|
||||||
|
exclude: [
|
||||||
|
'node_modules/',
|
||||||
|
'tests/',
|
||||||
|
'**/*.d.ts',
|
||||||
|
'**/*.config.*',
|
||||||
|
'**/dist/**',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.app.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
"@KTXC/*": ["../../core/src/*"],
|
||||||
|
"@DocumentsManager/*": ["../documents_manager/src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.tsx",
|
||||||
|
"src/**/*.vue",
|
||||||
|
"../documents_manager/src/**/*.ts",
|
||||||
|
"../../core/src/**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.node.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"vite.config.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
{
|
||||||
|
name: 'inject-css-filename',
|
||||||
|
enforce: 'post',
|
||||||
|
generateBundle(_options, bundle) {
|
||||||
|
const cssFile = Object.keys(bundle).find(name => name.endsWith('.css'))
|
||||||
|
if (!cssFile) return
|
||||||
|
|
||||||
|
for (const fileName of Object.keys(bundle)) {
|
||||||
|
const chunk = bundle[fileName]
|
||||||
|
if (chunk.type === 'chunk' && chunk.code.includes('__CSS_FILENAME_PLACEHOLDER__')) {
|
||||||
|
chunk.code = chunk.code.replace(/__CSS_FILENAME_PLACEHOLDER__/g, `static/${cssFile}`)
|
||||||
|
console.log(`Injected CSS filename "static/${cssFile}" into ${fileName}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
define: {
|
||||||
|
'process.env.NODE_ENV': JSON.stringify('production'),
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
'@KTXC': path.resolve(__dirname, '../../core/src'),
|
||||||
|
'@DocumentsManager': path.resolve(__dirname, '../documents_manager/src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'static',
|
||||||
|
emptyOutDir: true,
|
||||||
|
sourcemap: true,
|
||||||
|
lib: {
|
||||||
|
entry: path.resolve(__dirname, 'src/main.ts'),
|
||||||
|
formats: ['es'],
|
||||||
|
fileName: () => 'module.mjs',
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: [
|
||||||
|
'vue',
|
||||||
|
'vue-router',
|
||||||
|
'pinia',
|
||||||
|
'@KTXC',
|
||||||
|
/^@DocumentsManager\//,
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
paths: (id) => {
|
||||||
|
if (id === '@KTXC') return '/js/ktxc.mjs'
|
||||||
|
if (id.startsWith('@DocumentsManager/')) {
|
||||||
|
return '/modules/documents_manager/static/module.mjs'
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
},
|
||||||
|
assetFileNames: (assetInfo) => {
|
||||||
|
if (assetInfo.name?.endsWith('.css')) {
|
||||||
|
return 'documents-text-editor-[hash].css'
|
||||||
|
}
|
||||||
|
return '[name]-[hash][extname]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user