fix: listview action menu (#64)

* refactor: add a valibot schema for package registry validation

* fix: list view action menu

* chore: bump version to 0.1.16 in package.json

* refactor: extract supabase package from api

* ci: remove NODE_OPTIONS from build step and improve error handling in getLatestNpmPkgVersion function
This commit is contained in:
Huakun Shen 2025-01-18 02:26:23 -05:00 committed by GitHub
parent 21a90259ac
commit 7a3b6f3983
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 209 additions and 103 deletions

View File

@ -51,8 +51,6 @@ jobs:
- name: Setup - name: Setup
run: pnpm prepare run: pnpm prepare
- name: Build - name: Build
env:
NODE_OPTIONS: --max-old-space-size=4096
run: pnpm build run: pnpm build
- name: JS Test - name: JS Test
if: matrix.os == 'ubuntu-24.04' if: matrix.os == 'ubuntu-24.04'

View File

@ -11,9 +11,11 @@ export function getLatestNpmPkgInfo(pkgName: string): Promise<Record<string, any
} }
export function getLatestNpmPkgVersion(pkgName: string): Promise<string> { export function getLatestNpmPkgVersion(pkgName: string): Promise<string> {
return getLatestNpmPkgInfo(pkgName).then( return getLatestNpmPkgInfo(pkgName)
(data) => v.parse(v.object({ version: v.string() }), data).version .then((data) => v.parse(v.object({ version: v.string() }), data).version)
) .catch((err) => {
throw new Error(`Failed to get latest version of ${pkgName}: ${err.message}`)
})
} }
/** /**

View File

@ -1,6 +1,6 @@
{ {
"name": "@kksh/desktop", "name": "@kksh/desktop",
"version": "0.1.15", "version": "0.1.16",
"description": "", "description": "",
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@ -16,7 +16,7 @@ interface AppStateAPI {
clearSearchTerm: () => void clearSearchTerm: () => void
get: () => AppState get: () => AppState
setLoadingBar: (loadingBar: boolean) => void setLoadingBar: (loadingBar: boolean) => void
setDefaultAction: (defaultAction: string) => void setDefaultAction: (defaultAction: string | null) => void
setActionPanel: (actionPanel?: ActionSchema.ActionPanel) => void setActionPanel: (actionPanel?: ActionSchema.ActionPanel) => void
} }
@ -32,7 +32,7 @@ function createAppState(): Writable<AppState> & AppStateAPI {
setLoadingBar: (loadingBar: boolean) => { setLoadingBar: (loadingBar: boolean) => {
store.update((state) => ({ ...state, loadingBar })) store.update((state) => ({ ...state, loadingBar }))
}, },
setDefaultAction: (defaultAction: string) => { setDefaultAction: (defaultAction: string | null) => {
store.update((state) => ({ ...state, defaultAction })) store.update((state) => ({ ...state, defaultAction }))
}, },
setActionPanel: (actionPanel?: ActionSchema.ActionPanel) => { setActionPanel: (actionPanel?: ActionSchema.ActionPanel) => {

View File

@ -0,0 +1,26 @@
import { get, writable, type Writable } from "svelte/store"
export interface KeyStoreAPI {
get: () => string[]
getSet: () => Set<string>
keydown: (key: string) => void
keyup: (key: string) => void
}
function createKeysStore(): Writable<string[]> & KeyStoreAPI {
const store = writable<string[]>([])
return {
...store,
get: () => get(store),
getSet: () => new Set(get(store)),
keydown: (key: string) => {
store.update((state) => [...state, key])
},
keyup: (key: string) => {
store.update((state) => state.filter((k) => k !== key))
}
}
}
export const keys = createKeysStore()

View File

@ -1,4 +1,5 @@
import { appState } from "@/stores" import { appState } from "@/stores"
import { keys } from "@/stores/keys"
import { toggleDevTools } from "@kksh/api/commands" import { toggleDevTools } from "@kksh/api/commands"
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow" import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { getCurrentWindow } from "@tauri-apps/api/window" import { getCurrentWindow } from "@tauri-apps/api/window"
@ -72,6 +73,7 @@ export function goHomeOrCloseOnEscapeWithInput(e: KeyboardEvent) {
} }
export async function globalKeyDownHandler(e: KeyboardEvent) { export async function globalKeyDownHandler(e: KeyboardEvent) {
keys.keydown(e.key)
const _platform = platform() const _platform = platform()
if ((_platform === "macos" && e.metaKey) || (_platform === "windows" && e.ctrlKey)) { if ((_platform === "macos" && e.metaKey) || (_platform === "windows" && e.ctrlKey)) {
if (e.key === ",") { if (e.key === ",") {
@ -96,6 +98,10 @@ export async function globalKeyDownHandler(e: KeyboardEvent) {
} }
} }
export function globalKeyUpHandler(e: KeyboardEvent) {
keys.keyup(e.key)
}
export function isLetter(letter: string): boolean { export function isLetter(letter: string): boolean {
if (letter.length != 1) return false if (letter.length != 1) return false
return letter.match(/[a-zA-Z]/) ? true : false return letter.match(/[a-zA-Z]/) ? true : false

View File

@ -3,7 +3,7 @@
import { appConfig, appState, extensions, quickLinks, winExtMap } from "@/stores" import { appConfig, appState, extensions, quickLinks, winExtMap } from "@/stores"
import { initDeeplink } from "@/utils/deeplink" import { initDeeplink } from "@/utils/deeplink"
import { updateAppHotkey } from "@/utils/hotkey" import { updateAppHotkey } from "@/utils/hotkey"
import { globalKeyDownHandler, goBackOrCloseOnEscape } from "@/utils/key" import { globalKeyDownHandler, globalKeyUpHandler, goBackOrCloseOnEscape } from "@/utils/key"
import { listenToWindowBlur } from "@/utils/tauri-events" import { listenToWindowBlur } from "@/utils/tauri-events"
import { isInMainWindow } from "@/utils/window" import { isInMainWindow } from "@/utils/window"
import { listenToKillProcessEvent, listenToRecordExtensionProcessEvent } from "@kksh/api/events" import { listenToKillProcessEvent, listenToRecordExtensionProcessEvent } from "@kksh/api/events"
@ -97,7 +97,7 @@
}) })
</script> </script>
<svelte:window on:keydown={globalKeyDownHandler} /> <svelte:window on:keydown={globalKeyDownHandler} on:keyup={globalKeyUpHandler} />
<ViewTransition /> <ViewTransition />
<AppContext {appConfig} {appState}> <AppContext {appConfig} {appState}>
{@render children()} {@render children()}

View File

@ -4,8 +4,7 @@
import { supabaseAPI } from "@/supabase" import { supabaseAPI } from "@/supabase"
import { goBackOnEscapeClearSearchTerm, goHomeOnEscapeClearSearchTerm } from "@/utils/key" import { goBackOnEscapeClearSearchTerm, goHomeOnEscapeClearSearchTerm } from "@/utils/key"
import { goBack, goHome } from "@/utils/route" import { goBack, goHome } from "@/utils/route"
import { SBExt, type Tables } from "@kksh/api/supabase" import { SBExt, type Tables } from "@kksh/supabase"
import { isUpgradable } from "@kksh/extension"
import type { ExtPublishMetadata } from "@kksh/supabase/models" import type { ExtPublishMetadata } from "@kksh/supabase/models"
import { Button, Command } from "@kksh/svelte5" import { Button, Command } from "@kksh/svelte5"
import { Constants } from "@kksh/ui" import { Constants } from "@kksh/ui"

View File

@ -1,8 +1,8 @@
import { appConfig, extensions, installedStoreExts } from "@/stores" import { appConfig, extensions, installedStoreExts } from "@/stores"
import { supabaseAPI } from "@/supabase" import { supabaseAPI } from "@/supabase"
import type { ExtPackageJsonExtra } from "@kksh/api/models" import type { ExtPackageJsonExtra } from "@kksh/api/models"
import { SBExt } from "@kksh/api/supabase"
import { isExtPathInDev, isUpgradable } from "@kksh/extension" import { isExtPathInDev, isUpgradable } from "@kksh/extension"
import { SBExt } from "@kksh/supabase"
import { error } from "@sveltejs/kit" import { error } from "@sveltejs/kit"
import { derived, get, type Readable } from "svelte/store" import { derived, get, type Readable } from "svelte/store"
import type { PageLoad } from "./$types" import type { PageLoad } from "./$types"

View File

@ -3,8 +3,8 @@
import { extensions, installedStoreExts } from "@/stores/extensions.js" import { extensions, installedStoreExts } from "@/stores/extensions.js"
import { supabaseAPI } from "@/supabase" import { supabaseAPI } from "@/supabase"
import { goBack } from "@/utils/route.js" import { goBack } from "@/utils/route.js"
import type { Tables } from "@kksh/api/supabase/types"
import { ExtPublishMetadata } from "@kksh/supabase/models" import { ExtPublishMetadata } from "@kksh/supabase/models"
import type { Tables } from "@kksh/supabase/types"
import { Button } from "@kksh/svelte5" import { Button } from "@kksh/svelte5"
import { cn } from "@kksh/svelte5/utils" import { cn } from "@kksh/svelte5/utils"
import { Constants } from "@kksh/ui" import { Constants } from "@kksh/ui"

View File

@ -1,8 +1,8 @@
import { extensions } from "@/stores" import { extensions } from "@/stores"
import { supabaseAPI } from "@/supabase" import { supabaseAPI } from "@/supabase"
import { KunkunExtManifest, type ExtPackageJsonExtra } from "@kksh/api/models" import { KunkunExtManifest, type ExtPackageJsonExtra } from "@kksh/api/models"
import type { Tables } from "@kksh/api/supabase/types"
import { ExtPublishMetadata } from "@kksh/supabase/models" import { ExtPublishMetadata } from "@kksh/supabase/models"
import type { Tables } from "@kksh/supabase/types"
import { error } from "@sveltejs/kit" import { error } from "@sveltejs/kit"
import { toast } from "svelte-sonner" import { toast } from "svelte-sonner"
import * as v from "valibot" import * as v from "valibot"

View File

@ -1,5 +1,5 @@
import type { Tables } from "@kksh/api/supabase/types"
import type { ExtPublishMetadata } from "@kksh/supabase/models" import type { ExtPublishMetadata } from "@kksh/supabase/models"
import type { Tables } from "@kksh/supabase/types"
export async function getInstallExtras( export async function getInstallExtras(
ext: Tables<"ext_publish"> & { metadata?: ExtPublishMetadata } ext: Tables<"ext_publish"> & { metadata?: ExtPublishMetadata }

View File

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { appState } from "@/stores/appState.js" import { appState } from "@/stores/appState.js"
import { keys } from "@/stores/keys"
import { winExtMap } from "@/stores/winExtMap.js" import { winExtMap } from "@/stores/winExtMap.js"
import { listenToFileDrop, listenToRefreshDevExt } from "@/utils/tauri-events.js" import { listenToFileDrop, listenToRefreshDevExt } from "@/utils/tauri-events.js"
import { isInMainWindow } from "@/utils/window.js" import { isInMainWindow } from "@/utils/window.js"
@ -23,13 +24,16 @@
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow" import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { readTextFile } from "@tauri-apps/plugin-fs" import { readTextFile } from "@tauri-apps/plugin-fs"
import { debug } from "@tauri-apps/plugin-log" import { debug } from "@tauri-apps/plugin-log"
import { platform } from "@tauri-apps/plugin-os"
import { goto } from "$app/navigation" import { goto } from "$app/navigation"
import { RPCChannel, WorkerParentIO } from "kkrpc/browser" import { RPCChannel, WorkerParentIO } from "kkrpc/browser"
import { onDestroy, onMount } from "svelte" import { onDestroy, onMount, tick } from "svelte"
import * as v from "valibot" import * as v from "valibot"
const { data } = $props() const { data } = $props()
let listviewInputRef = $state<HTMLInputElement | null>(null)
let { loadedExt, scriptPath, extInfoInDB } = $derived(data) let { loadedExt, scriptPath, extInfoInDB } = $derived(data)
let actionPanelOpen = $state(false)
let workerAPI: WorkerExtension | undefined = undefined let workerAPI: WorkerExtension | undefined = undefined
let unlistenRefreshWorkerExt: UnlistenFn | undefined let unlistenRefreshWorkerExt: UnlistenFn | undefined
let unlistenFileDrop: UnlistenFn | undefined let unlistenFileDrop: UnlistenFn | undefined
@ -46,6 +50,7 @@
const loadingBar = $derived($appState.loadingBar || extensionLoadingBar) const loadingBar = $derived($appState.loadingBar || extensionLoadingBar)
let loaded = $state(false) let loaded = $state(false)
let listview: Templates.ListView | undefined = $state(undefined) let listview: Templates.ListView | undefined = $state(undefined)
const _platform = platform()
async function goBack() { async function goBack() {
if (isInMainWindow()) { if (isInMainWindow()) {
@ -134,6 +139,14 @@
listViewContent = parsedListView listViewContent = parsedListView
} }
// on each render, also update the default action and action panel
if (listViewContent?.defaultAction) {
appState.setDefaultAction(listViewContent.defaultAction)
}
if (listViewContent?.actions) {
appState.setActionPanel(listViewContent.actions)
}
// if (parsedListView.updateDetailOnly) { // if (parsedListView.updateDetailOnly) {
// if (listViewContent) { // if (listViewContent) {
// listViewContent.detail = parsedListView.detail // listViewContent.detail = parsedListView.detail
@ -187,6 +200,8 @@
worker.terminate() worker.terminate()
worker = undefined worker = undefined
} }
appState.setDefaultAction(null)
appState.setActionPanel(undefined)
const workerScript = await readTextFile(scriptPath) const workerScript = await readTextFile(scriptPath)
const blob = new Blob([workerScript], { type: "application/javascript" }) const blob = new Blob([workerScript], { type: "application/javascript" })
const blobURL = URL.createObjectURL(blob) const blobURL = URL.createObjectURL(blob)
@ -248,13 +263,44 @@
extensionLoadingBar = false extensionLoadingBar = false
appState.setActionPanel(undefined) appState.setActionPanel(undefined)
}) })
$effect(() => {
void $keys
const keySet = keys.getSet()
if (
keySet.size === 2 &&
keySet.has(_platform === "macos" ? "Meta" : "Control") &&
keySet.has("k")
) {
console.log("open action panel")
actionPanelOpen = true
}
})
function onActionPanelBlur() {
setTimeout(() => {
listviewInputRef?.focus()
}, 300)
}
function onkeydown(e: KeyboardEvent) {
if (e.key === "Escape") {
console.log(document.activeElement)
console.log(document.activeElement?.nodeName)
if (document.activeElement?.nodeName === "INPUT") {
console.log("input")
}
}
}
</script> </script>
<svelte:window on:keydown={onkeydown} />
{#if loadingBar} {#if loadingBar}
<LoadingBar class="fixed left-0 top-0 w-full" color="white" /> <LoadingBar class="fixed left-0 top-0 w-full" color="white" />
{/if} {/if}
{#if loaded && listViewContent !== undefined} {#if loaded && listViewContent !== undefined}
<Templates.ListView <Templates.ListView
bind:inputRef={listviewInputRef}
bind:searchTerm bind:searchTerm
bind:searchBarPlaceholder bind:searchBarPlaceholder
bind:this={listview} bind:this={listview}
@ -275,19 +321,33 @@
workerAPI?.onSearchTermChange(searchTerm) workerAPI?.onSearchTermChange(searchTerm)
}} }}
onHighlightedItemChanged={(value: string) => { onHighlightedItemChanged={(value: string) => {
workerAPI?.onHighlightedListItemChanged(value) // workerAPI?.onHighlightedListItemChanged(value)
if (listViewContent?.defaultAction) { // if (listViewContent?.defaultAction) {
appState.setDefaultAction(listViewContent.defaultAction) // appState.setDefaultAction(listViewContent.defaultAction)
} // }
if (listViewContent?.actions) { // if (listViewContent?.actions) {
appState.setActionPanel(listViewContent.actions) // appState.setActionPanel(listViewContent.actions)
// }
try {
const parsedItem = v.parse(ListSchema.Item, JSON.parse(value))
if (parsedItem.defaultAction) {
appState.setDefaultAction(parsedItem.defaultAction)
}
if (parsedItem.actions) {
appState.setActionPanel(parsedItem.actions)
}
workerAPI?.onHighlightedListItemChanged(parsedItem.value)
} catch (error) {
console.error(error)
} }
}} }}
> >
{#snippet footer()} {#snippet footer()}
<GlobalCommandPaletteFooter <GlobalCommandPaletteFooter
defaultAction={$appState.defaultAction} bind:actionPanelOpen
actionPanel={$appState.actionPanel} actionPanel={$appState.actionPanel}
defaultAction={$appState.defaultAction ?? undefined}
{onActionPanelBlur}
onDefaultActionSelected={() => { onDefaultActionSelected={() => {
workerAPI?.onEnterPressedOnSearchBar() workerAPI?.onEnterPressedOnSearchBar()
}} }}

View File

@ -14,8 +14,6 @@
"./permissions": "./src/permissions/index.ts", "./permissions": "./src/permissions/index.ts",
"./dev": "./src/dev/index.ts", "./dev": "./src/dev/index.ts",
"./events": "./src/events.ts", "./events": "./src/events.ts",
"./supabase": "./src/supabase/index.ts",
"./supabase/types": "./src/supabase/database.types.ts",
"./package.json": "./package.json" "./package.json": "./package.json"
}, },
"license": "MIT", "license": "MIT",

View File

@ -1,2 +0,0 @@
export * from "./model"
export * from "./database.types"

View File

@ -1,19 +0,0 @@
import { Icon } from "@kksh/api/models"
import * as v from "valibot"
/***
* Correspond to `extensions` table in supabase
*/
export const SBExt = v.object({
identifier: v.string(),
name: v.string(),
created_at: v.string(),
downloads: v.number(),
short_description: v.string(),
long_description: v.string(),
version: v.string(),
api_version: v.optional(v.string()),
icon: Icon
})
export type SBExt = v.InferOutput<typeof SBExt>

View File

@ -8,7 +8,8 @@
"exports": { "exports": {
"./jsr": "./src/jsr/index.ts", "./jsr": "./src/jsr/index.ts",
"./npm": "./src/npm/index.ts", "./npm": "./src/npm/index.ts",
"./github": "./src/github.ts" "./github": "./src/github.ts",
"./models": "./src/models.ts"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",

View File

@ -7,6 +7,7 @@ import {
import { ExtPackageJson } from "@kksh/api/models" import { ExtPackageJson } from "@kksh/api/models"
import * as v from "valibot" import * as v from "valibot"
import { authenticatedUserIsMemberOfGitHubOrg, userIsPublicMemberOfGitHubOrg } from "../github" import { authenticatedUserIsMemberOfGitHubOrg, userIsPublicMemberOfGitHubOrg } from "../github"
import type { ExtensionPublishValidationData } from "../models"
import type { NpmPkgMetadata } from "../npm/models" import type { NpmPkgMetadata } from "../npm/models"
import { getInfoFromRekorLog } from "../sigstore" import { getInfoFromRekorLog } from "../sigstore"
import { getTarballSize } from "../utils" import { getTarballSize } from "../utils"
@ -220,21 +221,7 @@ export async function validateJsrPackageAsKunkunExtension(payload: {
githubToken?: string githubToken?: string
}): Promise<{ }): Promise<{
error?: string error?: string
data?: { data?: ExtensionPublishValidationData
pkgJson: ExtPackageJson
tarballUrl: string
shasum: string
apiVersion: string
tarballSize: number
rekorLogIndex: string
github: {
githubActionInvocationId: string
commit: string
repo: string
owner: string
workflowPath: string
}
}
}> { }> {
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* check if jsr package exists */ /* check if jsr package exists */

View File

@ -1,3 +1,4 @@
import { ExtPackageJson } from "@kksh/api/models"
import * as v from "valibot" import * as v from "valibot"
export const RawRekorLogEntry = v.object({ export const RawRekorLogEntry = v.object({
@ -58,3 +59,19 @@ export const SigstoreAttestation = v.object({
}) })
}) })
export type SigstoreAttestation = v.InferOutput<typeof SigstoreAttestation> export type SigstoreAttestation = v.InferOutput<typeof SigstoreAttestation>
export const ExtensionPublishValidationData = v.object({
pkgJson: ExtPackageJson,
tarballUrl: v.string(),
shasum: v.string(),
apiVersion: v.string(),
rekorLogIndex: v.string(),
tarballSize: v.number(),
github: v.object({
githubActionInvocationId: v.string(),
commit: v.string(),
repo: v.string(),
owner: v.string(),
workflowPath: v.string()
})
})
export type ExtensionPublishValidationData = v.InferOutput<typeof ExtensionPublishValidationData>

View File

@ -5,6 +5,7 @@ import {
parseGitHubRepoFromUri, parseGitHubRepoFromUri,
userIsPublicMemberOfGitHubOrg userIsPublicMemberOfGitHubOrg
} from "../github" } from "../github"
import type { ExtensionPublishValidationData } from "../models"
import { getInfoFromRekorLog } from "../sigstore" import { getInfoFromRekorLog } from "../sigstore"
import { import {
NpmPkgMetadata, NpmPkgMetadata,
@ -116,21 +117,7 @@ export async function validateNpmPackageAsKunkunExtension(payload: {
provenance?: Provenance // provenance API has cors policy, when we run this validation on client side, a provenance should be passed in provenance?: Provenance // provenance API has cors policy, when we run this validation on client side, a provenance should be passed in
}): Promise<{ }): Promise<{
error?: string error?: string
data?: { data?: ExtensionPublishValidationData
pkgJson: ExtPackageJson
tarballUrl: string
shasum: string
apiVersion: string
rekorLogIndex: string
tarballSize: number
github: {
githubActionInvocationId: string
commit: string
repo: string
owner: string
workflowPath: string
}
}
}> { }> {
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* check if npm package exist */ /* check if npm package exist */

View File

@ -6,7 +6,8 @@
}, },
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./models": "./src/models.ts" "./models": "./src/models.ts",
"./types": "./src/database.types.ts"
}, },
"dependencies": { "dependencies": {
"@kksh/api": "workspace:*", "@kksh/api": "workspace:*",

View File

@ -1,7 +1,7 @@
import { SBExt } from "@kksh/api/supabase"
import type { Database, Tables } from "@kksh/api/supabase/types"
import type { PostgrestSingleResponse, SupabaseClient } from "@supabase/supabase-js" import type { PostgrestSingleResponse, SupabaseClient } from "@supabase/supabase-js"
import * as v from "valibot" import * as v from "valibot"
import type { Database, Tables } from "./database.types"
import { SBExt } from "./models"
export class SupabaseAPI { export class SupabaseAPI {
constructor(private supabase: SupabaseClient<Database>) {} constructor(private supabase: SupabaseClient<Database>) {}

View File

@ -1,5 +1,5 @@
import type { Database } from "@kksh/api/supabase/types"
import { createClient } from "@supabase/supabase-js" import { createClient } from "@supabase/supabase-js"
import type { Database } from "./database.types"
export function createSB(supabaseUrl: string, supabaseAnonKey: string) { export function createSB(supabaseUrl: string, supabaseAnonKey: string) {
return createClient<Database>(supabaseUrl, supabaseAnonKey, { return createClient<Database>(supabaseUrl, supabaseAnonKey, {
@ -10,5 +10,5 @@ export function createSB(supabaseUrl: string, supabaseAnonKey: string) {
} }
export { SupabaseAPI } from "./api" export { SupabaseAPI } from "./api"
export type { Database, Tables } from "@kksh/api/supabase/types" export type { Database, Tables } from "./database.types"
export { SBExt } from "@kksh/api/supabase" export { SBExt } from "./models"

View File

@ -2,6 +2,7 @@
* @module @kksh/supabase/models * @module @kksh/supabase/models
* This module contains some models for supabase database that cannot be code generated, such as JSON fields. * This module contains some models for supabase database that cannot be code generated, such as JSON fields.
*/ */
import { Icon } from "@kksh/api/models"
import * as v from "valibot" import * as v from "valibot"
export enum ExtPublishSourceTypeEnum { export enum ExtPublishSourceTypeEnum {
@ -24,3 +25,20 @@ export const ExtPublishMetadata = v.object({
) )
}) })
export type ExtPublishMetadata = v.InferOutput<typeof ExtPublishMetadata> export type ExtPublishMetadata = v.InferOutput<typeof ExtPublishMetadata>
/***
* Correspond to `extensions` table in supabase
*/
export const SBExt = v.object({
identifier: v.string(),
name: v.string(),
created_at: v.string(),
downloads: v.number(),
short_description: v.string(),
long_description: v.string(),
version: v.string(),
api_version: v.optional(v.string()),
icon: Icon
})
export type SBExt = v.InferOutput<typeof SBExt>

View File

@ -1,4 +1,3 @@
## Permission Table ## Permission Table
<table> <table>
@ -7,7 +6,6 @@
<th>Description</th> <th>Description</th>
</tr> </tr>
<tr> <tr>
<td> <td>

View File

@ -4,6 +4,6 @@ export interface AppState {
searchTerm: string searchTerm: string
highlightedCmd: string highlightedCmd: string
loadingBar: boolean loadingBar: boolean
defaultAction: string defaultAction: string | null
actionPanel?: ActionSchema.ActionPanel actionPanel?: ActionSchema.ActionPanel
} }

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import Icon from "@iconify/svelte" import Icon from "@iconify/svelte"
import { Icon as TIcon } from "@kksh/api/models" import { Icon as TIcon } from "@kksh/api/models"
import { SBExt } from "@kksh/api/supabase" import { SBExt } from "@kksh/supabase"
import { Button, Command } from "@kksh/svelte5" import { Button, Command } from "@kksh/svelte5"
import { Constants, IconMultiplexer } from "@kksh/ui" import { Constants, IconMultiplexer } from "@kksh/ui"
import { cn, humanReadableNumber } from "@kksh/ui/utils" import { cn, humanReadableNumber } from "@kksh/ui/utils"

View File

@ -2,8 +2,8 @@
import autoAnimate from "@formkit/auto-animate" import autoAnimate from "@formkit/auto-animate"
import Icon from "@iconify/svelte" import Icon from "@iconify/svelte"
import { ExtPackageJson, IconEnum, KunkunExtManifest } from "@kksh/api/models" import { ExtPackageJson, IconEnum, KunkunExtManifest } from "@kksh/api/models"
import { type Tables } from "@kksh/api/supabase/types"
import { ExtPublishMetadata, ExtPublishSourceTypeEnum } from "@kksh/supabase/models" import { ExtPublishMetadata, ExtPublishSourceTypeEnum } from "@kksh/supabase/models"
import { type Tables } from "@kksh/supabase/types"
import { Badge, Button, ScrollArea, Separator } from "@kksh/svelte5" import { Badge, Button, ScrollArea, Separator } from "@kksh/svelte5"
import { Constants, IconMultiplexer } from "@kksh/ui" import { Constants, IconMultiplexer } from "@kksh/ui"
import { cn } from "@kksh/ui/utils" import { cn } from "@kksh/ui/utils"
@ -163,7 +163,7 @@
href={`https://github.com/${metadata.git.owner}/${metadata.git.repo}/tree/${metadata.git.commit}`} href={`https://github.com/${metadata.git.owner}/${metadata.git.repo}/tree/${metadata.git.commit}`}
target="_blank" target="_blank"
> >
<Badge class="h-8 space-x-2"> <Badge class="h-8 space-x-2" variant="secondary">
<Icon class="h-6 w-6" icon="mdi:github" /> <Icon class="h-6 w-6" icon="mdi:github" />
<span>{metadata.git.owner}/{metadata.git.repo}</span> <span>{metadata.git.owner}/{metadata.git.repo}</span>
</Badge> </Badge>

View File

@ -4,7 +4,7 @@
import { Button, Checkbox, Form, Input, Label, Select } from "@kksh/svelte5" import { Button, Checkbox, Form, Input, Label, Select } from "@kksh/svelte5"
import { DatePickerWithPreset, Shiki } from "@kksh/ui" import { DatePickerWithPreset, Shiki } from "@kksh/ui"
import { buildFormSchema, cn } from "@kksh/ui/utils" import { buildFormSchema, cn } from "@kksh/ui/utils"
import { onMount } from "svelte" import { onMount, tick } from "svelte"
import SuperDebug, { defaults, superForm } from "sveltekit-superforms" import SuperDebug, { defaults, superForm } from "sveltekit-superforms"
import { valibot, valibotClient } from "sveltekit-superforms/adapters" import { valibot, valibotClient } from "sveltekit-superforms/adapters"
import * as v from "valibot" import * as v from "valibot"
@ -20,9 +20,17 @@
class?: string class?: string
onSubmit?: (formData: Record<string, string | number | boolean>) => void onSubmit?: (formData: Record<string, string | number | boolean>) => void
} = $props() } = $props()
let formRef = $state<HTMLFormElement | null>(null)
const formSchema = $derived(buildFormSchema(formViewContent)) const formSchema = $derived(buildFormSchema(formViewContent))
onMount(() => { onMount(() => {
console.log(formSchema) // auto focus the first input element
const input = formRef?.querySelector("input")
setTimeout(() => {
if (input) {
input.focus()
}
}, 300)
}) })
const form = $derived( const form = $derived(
superForm(defaults(valibot(formSchema)), { superForm(defaults(valibot(formSchema)), {
@ -51,7 +59,7 @@
{/if} {/if}
{/snippet} {/snippet}
{#key formViewContent} {#key formViewContent}
<form class={cn("flex flex-col gap-2", className)} use:enhance> <form class={cn("flex flex-col gap-2", className)} use:enhance bind:this={formRef}>
{#each formViewContent.fields as field} {#each formViewContent.fields as field}
{@const _field = field as FormSchema.BaseField} {@const _field = field as FormSchema.BaseField}
{#if _field.label && !_field.hideLabel} {#if _field.label && !_field.hideLabel}

View File

@ -61,7 +61,7 @@
$effect(() => { $effect(() => {
if (highlightedValue.startsWith("{")) { if (highlightedValue.startsWith("{")) {
onHighlightedItemChanged?.(JSON.parse(highlightedValue).value) onHighlightedItemChanged?.(highlightedValue)
} }
}) })

View File

@ -10,11 +10,13 @@
let { let {
actionPanel, actionPanel,
open = $bindable(false), open = $bindable(false),
onActionSelected onActionSelected,
onBlur
}: { }: {
actionPanel?: ActionSchema.ActionPanel actionPanel?: ActionSchema.ActionPanel
open?: boolean open?: boolean
onActionSelected?: (value: string) => void onActionSelected?: (value: string) => void
onBlur?: () => void
} = $props() } = $props()
let value = $state("") let value = $state("")
@ -24,16 +26,19 @@
// an item from the list so users can continue navigating the // an item from the list so users can continue navigating the
// rest of the form with the keyboard. // rest of the form with the keyboard.
function closeAndFocusTrigger() { function closeAndFocusTrigger() {
console.log("closeAndFocusTrigger")
open = false open = false
tick().then(() => { onBlur?.()
triggerRef.focus() // tick().then(() => {
}) // triggerRef.focus()
// })
} }
</script> </script>
<Popover.Root bind:open> <Popover.Root bind:open>
<Popover.Trigger bind:ref={triggerRef}> <Popover.Trigger bind:ref={triggerRef}>
{#snippet child({ props })} <!-- eslint-disable-next-line @typescript-eslint/no-explicit-any -->
{#snippet child({ props }: { props: any })}
<Button variant="ghost" class="" {...props} role="combobox" aria-expanded={open}> <Button variant="ghost" class="" {...props} role="combobox" aria-expanded={open}>
Actions Actions
<span class="flex items-center gap-0.5" data-tauri-drag-region> <span class="flex items-center gap-0.5" data-tauri-drag-region>
@ -46,7 +51,14 @@
</Popover.Trigger> </Popover.Trigger>
<Popover.Content class="w-64 p-0"> <Popover.Content class="w-64 p-0">
<Command.Root> <Command.Root>
<Command.Input placeholder="Select an Action" /> <Command.Input
placeholder="Select an Action"
onkeydown={(e) => {
if (e.key === "Escape") {
closeAndFocusTrigger()
}
}}
/>
<Command.List> <Command.List>
<Command.Empty>No action found.</Command.Empty> <Command.Empty>No action found.</Command.Empty>
<Command.Group> <Command.Group>

View File

@ -6,15 +6,19 @@
import Kbd from "../common/Kbd.svelte" import Kbd from "../common/Kbd.svelte"
import ActionPanel from "./ActionPanel.svelte" import ActionPanel from "./ActionPanel.svelte"
const { let {
class: className, class: className,
defaultAction, defaultAction,
actionPanel, actionPanel,
actionPanelOpen = $bindable(false),
onActionPanelBlur,
onDefaultActionSelected, onDefaultActionSelected,
onActionSelected onActionSelected
}: { }: {
class?: string class?: string
defaultAction?: string defaultAction?: string | null
actionPanelOpen?: boolean
onActionPanelBlur?: () => void
actionPanel?: ActionSchema.ActionPanel actionPanel?: ActionSchema.ActionPanel
onDefaultActionSelected?: () => void onDefaultActionSelected?: () => void
onActionSelected?: (value: string) => void onActionSelected?: (value: string) => void
@ -40,7 +44,12 @@
</Button> </Button>
{/if} {/if}
{#if actionPanel} {#if actionPanel}
<ActionPanel {actionPanel} {onActionSelected} /> <ActionPanel
{actionPanel}
{onActionSelected}
bind:open={actionPanelOpen}
onBlur={onActionPanelBlur}
/>
{/if} {/if}
</div> </div>
</div> </div>