Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c025e7af95 | ||
|
|
3bad311e35 | ||
|
|
fdc4fe0a3e | ||
|
|
5df2308bdb | ||
|
|
f4c44ce1aa | ||
|
|
49232f11cc | ||
|
|
1258eff42d | ||
|
|
718d46e3f9 | ||
|
|
92d9c40a28 |
+3
-1
@@ -40,7 +40,9 @@ async def watch(req, ws):
|
|||||||
if sso.paskia_enabled():
|
if sso.paskia_enabled():
|
||||||
# SSO auth: call validation to get user info (don't enforce auth in public mode)
|
# SSO auth: call validation to get user info (don't enforce auth in public mode)
|
||||||
try:
|
try:
|
||||||
await sso.validate_sso_request(req)
|
# WebSocket cannot forward Set-Cookie, so ask the auth backend not to
|
||||||
|
# renew the session here; renewal happens on the HTTP side instead.
|
||||||
|
await sso.validate_sso_request(req, renew=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug("watch SSO validation failed: %s", e)
|
logger.debug("watch SSO validation failed: %s", e)
|
||||||
if sso_user := getattr(req.ctx, "sso_user", None):
|
if sso_user := getattr(req.ctx, "sso_user", None):
|
||||||
|
|||||||
+1
-1
@@ -86,7 +86,7 @@ async def log_access(req, res):
|
|||||||
path = f"{path}?{qs}"
|
path = f"{path}?{qs}"
|
||||||
extra = getattr(req.ctx, "log_extra", None)
|
extra = getattr(req.ctx, "log_extra", None)
|
||||||
line = format_access_log(
|
line = format_access_log(
|
||||||
client, res.status, req.method, host, path, duration_ms, extra=extra
|
client, res.status, req.method, host, path, duration_ms=duration_ms, extra=extra
|
||||||
)
|
)
|
||||||
access_logger.info(line)
|
access_logger.info(line)
|
||||||
return res
|
return res
|
||||||
|
|||||||
+3
-1
@@ -145,6 +145,9 @@ class _PreviewWorker:
|
|||||||
|
|
||||||
async def kill(self) -> None:
|
async def kill(self) -> None:
|
||||||
if self.proc.returncode is None:
|
if self.proc.returncode is None:
|
||||||
|
# Safe to hard-kill: the worker is stateless per request, and its
|
||||||
|
# subprocesses (ffmpeg) use stdin=DEVNULL so they never hold the
|
||||||
|
# worker's pipes open — proc.wait() cannot hang on pipe EOF.
|
||||||
with contextlib.suppress(ProcessLookupError):
|
with contextlib.suppress(ProcessLookupError):
|
||||||
self.proc.kill()
|
self.proc.kill()
|
||||||
await self.proc.wait()
|
await self.proc.wait()
|
||||||
@@ -179,7 +182,6 @@ class _PreviewWorkerPool:
|
|||||||
stdin=asyncio.subprocess.PIPE,
|
stdin=asyncio.subprocess.PIPE,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
start_new_session=True,
|
|
||||||
)
|
)
|
||||||
_active_procs.add(proc)
|
_active_procs.add(proc)
|
||||||
try:
|
try:
|
||||||
|
|||||||
+16
-9
@@ -26,9 +26,9 @@ from pathlib import Path
|
|||||||
from time import perf_counter
|
from time import perf_counter
|
||||||
|
|
||||||
import av
|
import av
|
||||||
import fitz # PyMuPDF
|
|
||||||
import msgspec
|
import msgspec
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import pymupdf
|
||||||
import pyvips
|
import pyvips
|
||||||
from blake3 import blake3
|
from blake3 import blake3
|
||||||
|
|
||||||
@@ -219,7 +219,18 @@ def _image_via_ffmpeg(path: Path, maxsize: int, quality: int) -> bytes:
|
|||||||
cmd.insert(5, f"{new_w}x{new_h}")
|
cmd.insert(5, f"{new_w}x{new_h}")
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
subprocess.run(cmd, capture_output=True, check=True, shell=False) # noqa: S603
|
# stdin=DEVNULL is critical: ffmpeg must not inherit the worker's
|
||||||
|
# stdin, which carries the framed request protocol. An inherited
|
||||||
|
# stdin lets ffmpeg eat protocol bytes and, if the worker is
|
||||||
|
# killed mid-conversion, keeps the orphaned ffmpeg holding the
|
||||||
|
# pipe open so the parent's proc.wait() hangs forever.
|
||||||
|
subprocess.run( # noqa: S603
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
check=True,
|
||||||
|
shell=False,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
shell_cmd = shlex.join(cmd)
|
shell_cmd = shlex.join(cmd)
|
||||||
stderr = (e.stderr or b"").decode(errors="replace").strip()
|
stderr = (e.stderr or b"").decode(errors="replace").strip()
|
||||||
@@ -318,11 +329,11 @@ def process_image_buffer(data: bytes, *, quality, maxsize, maxzoom):
|
|||||||
|
|
||||||
def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0):
|
def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0):
|
||||||
t_load_start = perf_counter()
|
t_load_start = perf_counter()
|
||||||
pdf = fitz.open(path)
|
pdf = pymupdf.open(path)
|
||||||
page = pdf.load_page(page_number)
|
page = pdf.load_page(page_number)
|
||||||
w, h = page.rect[2:4]
|
w, h = page.rect[2:4]
|
||||||
zoom = min(maxsize / w, maxsize / h, maxzoom)
|
zoom = min(maxsize / w, maxsize / h, maxzoom)
|
||||||
mat = fitz.Matrix(zoom, zoom)
|
mat = pymupdf.Matrix(zoom, zoom)
|
||||||
pix = page.get_pixmap(matrix=mat)
|
pix = page.get_pixmap(matrix=mat)
|
||||||
t_load_end = perf_counter()
|
t_load_end = perf_counter()
|
||||||
|
|
||||||
@@ -540,11 +551,7 @@ def main() -> None:
|
|||||||
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
|
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
|
||||||
try:
|
try:
|
||||||
config.load_config()
|
config.load_config()
|
||||||
logger.warning(
|
logger.info("preview-worker config=%s", config.conffile)
|
||||||
"preview-worker config=%s master_secret=%s",
|
|
||||||
config.conffile,
|
|
||||||
config.config.secret,
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("preview-worker failed to load config at startup")
|
logger.exception("preview-worker failed to load config at startup")
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ def format_access_log(
|
|||||||
method: str,
|
method: str,
|
||||||
host: str,
|
host: str,
|
||||||
path: str,
|
path: str,
|
||||||
|
*,
|
||||||
duration_ms: float,
|
duration_ms: float,
|
||||||
extra: str | None = None,
|
extra: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
|||||||
+11
-1
@@ -62,12 +62,18 @@ async def close_client():
|
|||||||
_client = None
|
_client = None
|
||||||
|
|
||||||
|
|
||||||
async def validate_sso_request(request, *, perm: str = "cista:login") -> dict | None:
|
async def validate_sso_request(
|
||||||
|
request, *, perm: str = "cista:login", renew: bool = True
|
||||||
|
) -> dict | None:
|
||||||
"""Validate an SSO request against the auth backend.
|
"""Validate an SSO request against the auth backend.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request: The Sanic request object
|
request: The Sanic request object
|
||||||
perm: Permission to validate (default: cista:login, privileged also cista:admin)
|
perm: Permission to validate (default: cista:login, privileged also cista:admin)
|
||||||
|
renew: Whether to allow the auth backend to renew the session cookie.
|
||||||
|
Use ``False`` for WebSocket validation where Set-Cookie cannot be
|
||||||
|
forwarded to the client; this makes the request read-only and avoids
|
||||||
|
resetting the backend renewal timeout.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
User info dict if valid, None if validation fails with auth required response
|
User info dict if valid, None if validation fails with auth required response
|
||||||
@@ -88,12 +94,16 @@ async def validate_sso_request(request, *, perm: str = "cista:login") -> dict |
|
|||||||
headers["cookie"] = request.headers["cookie"]
|
headers["cookie"] = request.headers["cookie"]
|
||||||
if "authorization" in request.headers:
|
if "authorization" in request.headers:
|
||||||
headers["authorization"] = request.headers["authorization"]
|
headers["authorization"] = request.headers["authorization"]
|
||||||
|
if "user-agent" in request.headers:
|
||||||
|
headers["user-agent"] = request.headers["user-agent"]
|
||||||
headers["accept"] = "application/json"
|
headers["accept"] = "application/json"
|
||||||
headers["x-forwarded-for"] = request.client_ip
|
headers["x-forwarded-for"] = request.client_ip
|
||||||
headers["x-forwarded-host"] = request.host
|
headers["x-forwarded-host"] = request.host
|
||||||
headers["x-forwarded-proto"] = request.scheme
|
headers["x-forwarded-proto"] = request.scheme
|
||||||
|
|
||||||
url = f"{PASKIA_BACKEND_URL}/auth/api/validate?perm={perm}"
|
url = f"{PASKIA_BACKEND_URL}/auth/api/validate?perm={perm}"
|
||||||
|
if not renew:
|
||||||
|
url += "&renew=0"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
|
|||||||
@@ -6,9 +6,8 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "run-p type-check \"build-only {@}\" --",
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test:unit": "vitest",
|
|
||||||
"build-only": "vite build",
|
"build-only": "vite build",
|
||||||
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
||||||
"lint": "biome lint .",
|
"lint": "biome lint .",
|
||||||
"format": "biome format --write .",
|
"format": "biome format --write .",
|
||||||
"format:check": "biome format --check .",
|
"format:check": "biome format --check .",
|
||||||
@@ -37,17 +36,13 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.9.4",
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@tsconfig/node18": "^18.2.6",
|
"@tsconfig/node18": "^18.2.6",
|
||||||
"@types/jsdom": "^27.0.0",
|
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^25.1.0",
|
"@types/node": "^25.1.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.3",
|
"@vitejs/plugin-vue": "^6.0.3",
|
||||||
"@vue/test-utils": "^2.4.6",
|
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
"jsdom": "^27.4.0",
|
|
||||||
"npm-run-all2": "^8.0.4",
|
"npm-run-all2": "^8.0.4",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vitest": "^4.0.18",
|
|
||||||
"vue-tsc": "^3.2.4"
|
"vue-tsc": "^3.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ const path: ComputedRef<Path> = computed(() => {
|
|||||||
const isEditorPath = !!(doc && !doc.dir && doc.text)
|
const isEditorPath = !!(doc && !doc.dir && doc.text)
|
||||||
const canonicalBase = !fullPath ? '/' : doc?.dir ? `/${fullPath}/` : `/${fullPath}`
|
const canonicalBase = !fullPath ? '/' : doc?.dir ? `/${fullPath}/` : `/${fullPath}`
|
||||||
const canonicalPath = query
|
const canonicalPath = query
|
||||||
? rawPath // keep search URL shape untouched
|
? `${rawPath}//${query}` // keep search URL shape untouched
|
||||||
: canonicalBase
|
: canonicalBase
|
||||||
const pathList = isEditorPath ? routePathList.slice(0, -1) : routePathList
|
const pathList = isEditorPath ? routePathList.slice(0, -1) : routePathList
|
||||||
const breadcrumbPathList = routePathList
|
const breadcrumbPathList = routePathList
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
<a :href="doc.text ? doc.editurl : doc.url" tabindex=-1 @contextmenu.stop @focus.stop="store.cursor = doc.key">
|
<a :href="doc.text ? doc.editurl : doc.url" tabindex=-1 @contextmenu.stop @focus.stop="store.cursor = doc.key">
|
||||||
{{ doc.name }}
|
{{ doc.name }}
|
||||||
</a>
|
</a>
|
||||||
<button tabindex=-1 v-if="store.cursor == doc.key" class="rename-button" @click="() => (editing = doc)">🖊️</button>
|
<button tabindex=-1 class="rename-button" @click="() => (editing = doc)">🖊️</button>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<FileModified :doc=doc :now=nowkey />
|
<FileModified :doc=doc :now=nowkey />
|
||||||
@@ -84,6 +84,7 @@ import ContextMenu from '@imengyu/vue3-context-menu'
|
|||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
nextTick,
|
nextTick,
|
||||||
|
onActivated,
|
||||||
onDeactivated,
|
onDeactivated,
|
||||||
onMounted,
|
onMounted,
|
||||||
onUnmounted,
|
onUnmounted,
|
||||||
@@ -336,9 +337,13 @@ const focusBreadcrumb = () => {
|
|||||||
const keyboardFollowScroll = createKeyboardFollowScroll()
|
const keyboardFollowScroll = createKeyboardFollowScroll()
|
||||||
const markKeyboardFollow = keyboardFollowScroll.markKeyboardFollow
|
const markKeyboardFollow = keyboardFollowScroll.markKeyboardFollow
|
||||||
const keepCursorVisibleSmooth = keyboardFollowScroll.keepVisible
|
const keepCursorVisibleSmooth = keyboardFollowScroll.keepVisible
|
||||||
|
// Deactivated (KeepAlive-cached) instances stay alive with frozen, potentially
|
||||||
|
// stale props - their watchers must not react to global store changes.
|
||||||
|
let isActive = true
|
||||||
watch(
|
watch(
|
||||||
() => store.cursor,
|
() => store.cursor,
|
||||||
cursor => {
|
cursor => {
|
||||||
|
if (!isActive) return
|
||||||
if (cursor && editing.value && cursor !== editing.value.key) {
|
if (cursor && editing.value && cursor !== editing.value.key) {
|
||||||
exitEditing()
|
exitEditing()
|
||||||
}
|
}
|
||||||
@@ -347,6 +352,7 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => store.cursor,
|
() => store.cursor,
|
||||||
cursor => {
|
cursor => {
|
||||||
|
if (!isActive) return
|
||||||
if (cursor && !editing.value) {
|
if (cursor && !editing.value) {
|
||||||
const a = document.querySelector(
|
const a = document.querySelector(
|
||||||
`#file-${cursor} .name a`
|
`#file-${cursor} .name a`
|
||||||
@@ -359,6 +365,7 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => [props.documents.length, store.cursor, store.query, editing.value] as const,
|
() => [props.documents.length, store.cursor, store.query, editing.value] as const,
|
||||||
([len, cursor, query, editingDoc]) => {
|
([len, cursor, query, editingDoc]) => {
|
||||||
|
if (!isActive) return
|
||||||
if (!len && cursor && !query && !editingDoc) {
|
if (!len && cursor && !query && !editingDoc) {
|
||||||
store.cursor = ''
|
store.cursor = ''
|
||||||
focusBreadcrumb()
|
focusBreadcrumb()
|
||||||
@@ -378,7 +385,11 @@ onMounted(() => {
|
|||||||
active.focus({ preventScroll: true })
|
active.focus({ preventScroll: true })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
onActivated(() => {
|
||||||
|
isActive = true
|
||||||
|
})
|
||||||
onDeactivated(() => {
|
onDeactivated(() => {
|
||||||
|
isActive = false
|
||||||
if (editing.value) exitEditing()
|
if (editing.value) exitEditing()
|
||||||
})
|
})
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -617,6 +628,12 @@ table td {
|
|||||||
.name .rename-button {
|
.name .rename-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
tbody tr:hover .name .rename-button {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
animation: appear calc(5 * var(--transition-time)) linear;
|
animation: appear calc(5 * var(--transition-time)) linear;
|
||||||
}
|
}
|
||||||
@keyframes appear {
|
@keyframes appear {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ input#FileRenameInput {
|
|||||||
padding: .75em;
|
padding: .75em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
:editing="editing === doc ? {rename, exit} : null"
|
:editing="editing === doc ? {rename, exit} : null"
|
||||||
:style="{ '--gallery-figure-height': rowHeightsByKey[doc.key] ?? '15em' }"
|
:style="{ '--gallery-figure-height': rowHeightsByKey[doc.key] ?? '15em' }"
|
||||||
@menu="contextMenu($event, doc)"
|
@menu="contextMenu($event, doc)"
|
||||||
@rename="editing = doc; store.cursor = doc.key"
|
@rename="onFigureRename(doc)"
|
||||||
:class="{ 'folder-start': showFolderBreadcrumb(index) }"
|
:class="{ 'folder-start': showFolderBreadcrumb(index) }"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -64,6 +64,10 @@ const editing = shallowRef<Doc | null>(null)
|
|||||||
const exit = () => {
|
const exit = () => {
|
||||||
editing.value = null
|
editing.value = null
|
||||||
}
|
}
|
||||||
|
const onFigureRename = (doc: Doc) => {
|
||||||
|
editing.value = doc
|
||||||
|
store.cursor = doc.key
|
||||||
|
}
|
||||||
const rename = async (doc: Doc, newName: string) => {
|
const rename = async (doc: Doc, newName: string) => {
|
||||||
const oldName = doc.name
|
const oldName = doc.name
|
||||||
doc.name = newName // We should get an update from watch but this is quicker
|
doc.name = newName // We should get an update from watch but this is quicker
|
||||||
@@ -396,9 +400,13 @@ const focusBreadcrumb = () => {
|
|||||||
const keyboardFollowScroll = createKeyboardFollowScroll()
|
const keyboardFollowScroll = createKeyboardFollowScroll()
|
||||||
const markKeyboardFollow = keyboardFollowScroll.markKeyboardFollow
|
const markKeyboardFollow = keyboardFollowScroll.markKeyboardFollow
|
||||||
const keepCursorVisibleSmooth = keyboardFollowScroll.keepVisible
|
const keepCursorVisibleSmooth = keyboardFollowScroll.keepVisible
|
||||||
|
// Deactivated (KeepAlive-cached) instances stay alive with frozen, potentially
|
||||||
|
// stale props - their watchers must not react to global store changes.
|
||||||
|
let isActive = true
|
||||||
watch(
|
watch(
|
||||||
() => store.cursor,
|
() => store.cursor,
|
||||||
cursor => {
|
cursor => {
|
||||||
|
if (!isActive) return
|
||||||
if (cursor && editing.value && cursor !== editing.value.key) {
|
if (cursor && editing.value && cursor !== editing.value.key) {
|
||||||
exit()
|
exit()
|
||||||
}
|
}
|
||||||
@@ -407,6 +415,7 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => store.cursor,
|
() => store.cursor,
|
||||||
cursor => {
|
cursor => {
|
||||||
|
if (!isActive) return
|
||||||
if (cursor && !editing.value) {
|
if (cursor && !editing.value) {
|
||||||
const a = document.querySelector(`#file-${cursor}`) as HTMLAnchorElement | null
|
const a = document.querySelector(`#file-${cursor}`) as HTMLAnchorElement | null
|
||||||
if (a) {
|
if (a) {
|
||||||
@@ -419,6 +428,7 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => [props.documents.length, store.cursor, store.query, editing.value] as const,
|
() => [props.documents.length, store.cursor, store.query, editing.value] as const,
|
||||||
([len, cursor, query, editingDoc]) => {
|
([len, cursor, query, editingDoc]) => {
|
||||||
|
if (!isActive) return
|
||||||
if (!len && cursor && !query && !editingDoc) {
|
if (!len && cursor && !query && !editingDoc) {
|
||||||
store.cursor = ''
|
store.cursor = ''
|
||||||
focusBreadcrumb()
|
focusBreadcrumb()
|
||||||
@@ -449,12 +459,14 @@ onMounted(() => {
|
|||||||
attachGalleryObservers()
|
attachGalleryObservers()
|
||||||
})
|
})
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
|
isActive = true
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
updateColumns()
|
updateColumns()
|
||||||
attachGalleryObservers()
|
attachGalleryObservers()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
onDeactivated(() => {
|
onDeactivated(() => {
|
||||||
|
isActive = false
|
||||||
detachGalleryObservers()
|
detachGalleryObservers()
|
||||||
if (editing.value) exit()
|
if (editing.value) exit()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<span class="filename">{{ snap.displayName }}<SparseIndicator :doc="doc" class="after-name" /></span>
|
<span class="filename">{{ snap.displayName }}<SparseIndicator :doc="doc" class="after-name" /></span>
|
||||||
<span v-if="snap.ext" class="file-ext">.{{ snap.ext }}</span>
|
<span v-if="snap.ext" class="file-ext">.{{ snap.ext }}</span>
|
||||||
</span>
|
</span>
|
||||||
<button class="rename-btn" @click="$emit('rename')" title="Rename">✏️</button>
|
<button class="rename-btn" @click="emit('rename')" title="Rename">✏️</button>
|
||||||
</div>
|
</div>
|
||||||
<div class=namespacer></div>
|
<div class=namespacer></div>
|
||||||
</template>
|
</template>
|
||||||
@@ -64,6 +64,10 @@ const props = defineProps<{
|
|||||||
doc: Doc
|
doc: Doc
|
||||||
editing?: EditingProp
|
editing?: EditingProp
|
||||||
}>()
|
}>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'rename'): void
|
||||||
|
(e: 'menu', ev: MouseEvent): void
|
||||||
|
}>()
|
||||||
const m = ref<typeof MediaPreview | null>(null)
|
const m = ref<typeof MediaPreview | null>(null)
|
||||||
const tooltip = ref<InstanceType<typeof CursorTooltip> | null>(null)
|
const tooltip = ref<InstanceType<typeof CursorTooltip> | null>(null)
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./tsconfig.app.json"
|
"path": "./tsconfig.app.json"
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.vitest.json"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": "@tsconfig/node18/tsconfig.json",
|
"extends": "@tsconfig/node18/tsconfig.json",
|
||||||
"include": [
|
"include": ["vite.config.*"],
|
||||||
"vite.config.*",
|
|
||||||
"vitest.config.*",
|
|
||||||
"cypress.config.*",
|
|
||||||
"nightwatch.conf.*",
|
|
||||||
"playwright.config.*"
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.app.json",
|
|
||||||
"exclude": [],
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"types": ["node", "jsdom"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -132,6 +132,7 @@ ignore = [
|
|||||||
"ANN205", # legacy codebase: no full runtime annotation coverage yet
|
"ANN205", # legacy codebase: no full runtime annotation coverage yet
|
||||||
"BLE001", # broad catch remains in boundary/proxy/error-handling paths
|
"BLE001", # broad catch remains in boundary/proxy/error-handling paths
|
||||||
"C901", # legacy complexity; keep other correctness rules enabled
|
"C901", # legacy complexity; keep other correctness rules enabled
|
||||||
|
"CPY", # copyright notices not wanted in this codebase
|
||||||
"D100", # legacy docs not yet standardized
|
"D100", # legacy docs not yet standardized
|
||||||
"D101", # legacy docs not yet standardized
|
"D101", # legacy docs not yet standardized
|
||||||
"D102", # legacy docs not yet standardized
|
"D102", # legacy docs not yet standardized
|
||||||
|
|||||||
Reference in New Issue
Block a user