WiP
This commit is contained in:
@@ -27,38 +27,57 @@
|
||||
<q-btn flat color="primary" label="Retry" @click="refresh" class="q-mt-sm" />
|
||||
</div>
|
||||
<q-list separator v-else>
|
||||
<q-item
|
||||
v-for="component in filteredComponents"
|
||||
:key="component.code"
|
||||
clickable
|
||||
:active="selectedComponent === component.code"
|
||||
@click="selectComponent(component.code)"
|
||||
active-class="bg-primary text-white"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon :name="component.icon" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ component.name }}</q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
:class="selectedComponent === component.code ? 'text-white' : 'text-grey'"
|
||||
>
|
||||
{{ component.description }}
|
||||
</q-item-label>
|
||||
<div v-if="component.tags.length" class="q-mt-xs">
|
||||
<q-badge
|
||||
v-for="tag in component.tags"
|
||||
:key="tag"
|
||||
color="secondary"
|
||||
class="q-mr-xs q-mb-xs"
|
||||
size="sm"
|
||||
<template v-for="component in filteredComponents" :key="component.code">
|
||||
<!-- Component item -->
|
||||
<q-item
|
||||
clickable
|
||||
:active="selectedComponent === component.code"
|
||||
@click="selectComponent(component.code)"
|
||||
active-class="bg-primary text-white"
|
||||
>
|
||||
<q-icon :name="component.icon" class="q-mr-sm" size="sm" />
|
||||
<q-item-section>
|
||||
<q-item-label>{{ component.name }}</q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
:class="selectedComponent === component.code ? 'text-white' : 'text-grey'"
|
||||
>
|
||||
{{ tag }}
|
||||
</q-badge>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
{{ component.description }}
|
||||
</q-item-label>
|
||||
<div v-if="component.tags.length" class="q-mt-xs">
|
||||
<q-badge
|
||||
v-for="tag in component.tags"
|
||||
:key="tag"
|
||||
color="secondary"
|
||||
class="q-mr-xs q-mb-xs"
|
||||
size="sm"
|
||||
>
|
||||
{{ tag }}
|
||||
</q-badge>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<!-- Preset sub-items (only shown if component is selected and has presets) -->
|
||||
<template v-if="component.presets && component.presets.length > 0 && selectedComponent === component.code">
|
||||
<q-item
|
||||
v-for="preset in component.presets"
|
||||
:key="preset.presetId"
|
||||
clickable
|
||||
class="preset-item"
|
||||
dense
|
||||
@click="selectPreset(preset.presetId)"
|
||||
>
|
||||
<q-icon name="mdi-clipboard-play-outline" size="xs" class="q-mr-sm" />
|
||||
<q-item-section>
|
||||
<q-item-label class="text-caption">{{ preset.presetName }}</q-item-label>
|
||||
<q-item-label caption class="text-grey">
|
||||
{{ preset.description }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</template>
|
||||
</q-list>
|
||||
</q-card>
|
||||
</template>
|
||||
@@ -79,6 +98,7 @@ const emit = defineEmits<{
|
||||
'update:selectedComponent': [code: string]
|
||||
'update:searchQuery': [query: string]
|
||||
refresh: []
|
||||
selectPreset: [presetId: string]
|
||||
}>()
|
||||
|
||||
const searchQueryModel = computed({
|
||||
@@ -90,7 +110,20 @@ const selectComponent = (code: string) => {
|
||||
emit('update:selectedComponent', code)
|
||||
}
|
||||
|
||||
const selectPreset = (presetId: string) => {
|
||||
emit('selectPreset', presetId)
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
emit('refresh')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.preset-item {
|
||||
padding-left: 32px;
|
||||
.q-item__section--side {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
@click="refreshComponents"
|
||||
title="Reload components"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="mdi-content-copy"
|
||||
@click="copyPreset"
|
||||
title="Copy current inputs as preset"
|
||||
/>
|
||||
<q-btn flat round dense icon="mdi-cog" />
|
||||
<q-btn
|
||||
flat
|
||||
@@ -32,6 +40,7 @@ import { useDarkMode } from 'src/composables/useDarkMode'
|
||||
|
||||
const emit = defineEmits<{
|
||||
refresh: []
|
||||
copyPreset: []
|
||||
}>()
|
||||
|
||||
const { toggleDarkMode, isDark } = useDarkMode()
|
||||
@@ -45,4 +54,8 @@ const toggleLeftMenu = () => {
|
||||
const refreshComponents = () => {
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
const copyPreset = () => {
|
||||
emit('copyPreset')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<div class="s8n-calculator q-pa-md">
|
||||
<div class="q-gutter-md">
|
||||
<q-select
|
||||
outlined
|
||||
v-model="inputs.operator"
|
||||
label="Operator"
|
||||
:options="[
|
||||
@@ -17,7 +16,6 @@
|
||||
<q-input
|
||||
v-for="(a, aidx) in inputs.args"
|
||||
:key="aidx"
|
||||
outlined
|
||||
v-model.number="inputs.args[aidx]"
|
||||
:label="`${aidx === 0 ? 'Base' : aidx === 1 ? 'Second' : aidx === 2 ? 'Third' : `${aidx + 1}th`} Number`"
|
||||
type="number"
|
||||
@@ -29,8 +27,6 @@
|
||||
class="col-auto"
|
||||
icon="mdi-delete"
|
||||
color="negative"
|
||||
outline
|
||||
dense
|
||||
></q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
@@ -80,10 +76,9 @@ defineExpose({
|
||||
<style lang="scss" scoped>
|
||||
.s8n-calculator {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
|
||||
.result-section {
|
||||
min-height: 3em;
|
||||
min-height: 2em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -2,18 +2,16 @@
|
||||
<div class="s8n-http-request q-pa-md">
|
||||
<div class="q-gutter-md">
|
||||
<!-- HTTP Method and URL -->
|
||||
<div class="row q-gutter-md">
|
||||
<div class="row">
|
||||
<q-select
|
||||
outlined
|
||||
v-model="inputs.method"
|
||||
label="Method"
|
||||
:options="httpMethods"
|
||||
emit-value
|
||||
map-options
|
||||
class="col-3"
|
||||
class="col-3 q-mr-sm"
|
||||
/>
|
||||
<q-input
|
||||
outlined
|
||||
v-model="inputs.url"
|
||||
label="URL"
|
||||
placeholder="https://api.example.com/data"
|
||||
@@ -31,27 +29,25 @@
|
||||
<div
|
||||
v-for="([key, value], index) in Object.entries(inputs.headers)"
|
||||
:key="index"
|
||||
class="row q-gutter-sm items-center"
|
||||
class="row items-center"
|
||||
>
|
||||
<q-input
|
||||
outlined
|
||||
:model-value="key"
|
||||
@update:model-value="updateHeaderKey(index, $event as string)"
|
||||
label="Header Name"
|
||||
dense
|
||||
class="col-4"
|
||||
class="col-4 q-mr-md"
|
||||
/>
|
||||
<q-input
|
||||
outlined
|
||||
:model-value="value"
|
||||
@update:model-value="updateHeaderValue(key, $event as string)"
|
||||
label="Header Value"
|
||||
dense
|
||||
class="col"
|
||||
/>
|
||||
<q-btn icon="mdi-delete" color="negative" outline dense @click="removeHeader(key)" />
|
||||
<q-btn icon="mdi-delete" color="negative" dense @click="removeHeader(key)" />
|
||||
</div>
|
||||
<q-btn icon="mdi-plus" label="Add Header" color="secondary" outline @click="addHeader" />
|
||||
<q-btn icon="mdi-plus" label="Add Header" color="secondary" @click="addHeader" />
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
|
||||
|
||||
@@ -15,6 +15,18 @@ export interface ComponentOutput {
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface PresetInfo {
|
||||
presetId: string
|
||||
presetName: string
|
||||
baseComponentCode: string
|
||||
description: string
|
||||
version: string
|
||||
author: string
|
||||
createdAt: string
|
||||
inputs: Record<string, unknown>
|
||||
metadata: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface ApiComponent {
|
||||
code: string
|
||||
name: string
|
||||
@@ -28,6 +40,7 @@ export interface ApiComponent {
|
||||
inputs: ComponentInput[]
|
||||
outputs: ComponentOutput[]
|
||||
source: string
|
||||
presets?: PresetInfo[]
|
||||
}
|
||||
|
||||
export interface ComponentInfo {
|
||||
@@ -44,10 +57,12 @@ export interface ComponentInfo {
|
||||
outputs: ComponentOutput[]
|
||||
source: string
|
||||
category: string
|
||||
presets: PresetInfo[]
|
||||
}
|
||||
|
||||
export function useComponentStore() {
|
||||
const selectedComponent = ref<string>('')
|
||||
const selectedPreset = ref<string>('')
|
||||
const availableComponents = ref<ComponentInfo[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
@@ -55,19 +70,9 @@ export function useComponentStore() {
|
||||
|
||||
// Map API component to ComponentInfo
|
||||
const mapApiComponent = (apiComponent: ApiComponent): ComponentInfo => {
|
||||
const shortCode = apiComponent.code.split('.').pop() || apiComponent.code
|
||||
const shortCode = apiComponent.code
|
||||
|
||||
let componentImport: AsyncComponentLoader
|
||||
switch (apiComponent.gui) {
|
||||
case 'ComponentCalculator.vue':
|
||||
componentImport = () => import('../components_s8n/ComponentCalculator.vue')
|
||||
break
|
||||
case 'ComponentHttpRequest.vue':
|
||||
componentImport = () => import('../components_s8n/ComponentHttpRequest.vue')
|
||||
break
|
||||
default:
|
||||
componentImport = () => import('../components_s8n/ComponentCalculator.vue')
|
||||
}
|
||||
const componentImport: AsyncComponentLoader = () => import('../components_s8n/' + apiComponent.gui)
|
||||
|
||||
return {
|
||||
code: shortCode,
|
||||
@@ -83,6 +88,7 @@ export function useComponentStore() {
|
||||
outputs: apiComponent.outputs,
|
||||
source: apiComponent.source,
|
||||
category: apiComponent.category,
|
||||
presets: apiComponent.presets || [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,8 +132,43 @@ export function useComponentStore() {
|
||||
})
|
||||
})
|
||||
|
||||
// Get preset by ID
|
||||
const getPresetById = (presetId: string): PresetInfo | undefined => {
|
||||
for (const component of availableComponents.value) {
|
||||
const preset = component.presets.find(p => p.presetId === presetId)
|
||||
if (preset) return preset
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Apply preset to component
|
||||
const applyPreset = (presetId: string) => {
|
||||
const preset = getPresetById(presetId)
|
||||
if (!preset) {
|
||||
console.error(`Preset not found: ${presetId}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Select the component if not already selected
|
||||
if (selectedComponent.value !== preset.baseComponentCode) {
|
||||
selectedComponent.value = preset.baseComponentCode
|
||||
}
|
||||
|
||||
selectedPreset.value = presetId
|
||||
|
||||
// Note: Actual application of inputs happens in PlaygroundPage.vue
|
||||
// which will watch for preset changes and apply to component
|
||||
return preset.inputs
|
||||
}
|
||||
|
||||
// Clear selected preset
|
||||
const clearSelectedPreset = () => {
|
||||
selectedPreset.value = ''
|
||||
}
|
||||
|
||||
return {
|
||||
selectedComponent,
|
||||
selectedPreset,
|
||||
availableComponents,
|
||||
loading,
|
||||
error,
|
||||
@@ -135,5 +176,8 @@ export function useComponentStore() {
|
||||
fetchComponents,
|
||||
currentComponentInfo,
|
||||
filteredComponents,
|
||||
getPresetById,
|
||||
applyPreset,
|
||||
clearSelectedPreset,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<q-page class="playground-page">
|
||||
<!-- Toolbar -->
|
||||
<PlaygroundToolbar @refresh="fetchComponents" />
|
||||
<PlaygroundToolbar @refresh="fetchComponents" @copy-preset="copyCurrentPreset" />
|
||||
|
||||
<div class="row q-pa-md q-col-gutter-md">
|
||||
<!-- Main Component Area -->
|
||||
@@ -29,6 +29,7 @@
|
||||
@update:selected-component="selectedComponent = $event"
|
||||
@update:search-query="searchQuery = $event"
|
||||
@refresh="fetchComponents"
|
||||
@select-preset="handlePresetSelect"
|
||||
/>
|
||||
|
||||
<!-- Component Info Card -->
|
||||
@@ -67,6 +68,7 @@ const {
|
||||
fetchComponents,
|
||||
currentComponentInfo,
|
||||
filteredComponents,
|
||||
applyPreset,
|
||||
} = useComponentStore()
|
||||
|
||||
// Call history
|
||||
@@ -182,6 +184,81 @@ const reproduceHistoryItem = async (item: CallHistoryItem) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle preset selection
|
||||
const handlePresetSelect = async (presetId: string) => {
|
||||
console.log('Selected preset:', presetId)
|
||||
|
||||
// Apply preset via store (this will select the component if needed)
|
||||
const presetInputs = applyPreset(presetId)
|
||||
if (!presetInputs) {
|
||||
console.error('Failed to apply preset:', presetId)
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for component to be loaded (next tick) then set inputs
|
||||
await nextTick()
|
||||
if (componentRef.value && componentRef.value.inputs) {
|
||||
// inputs is a Ref<TIn> from runtime.createExecutor
|
||||
const inputsRef = componentRef.value.inputs as { value: unknown }
|
||||
if (typeof inputsRef === 'object' && inputsRef !== null && 'value' in inputsRef) {
|
||||
inputsRef.value = presetInputs
|
||||
} else {
|
||||
// Fallback: assign directly (might not be reactive)
|
||||
componentRef.value.inputs = presetInputs
|
||||
}
|
||||
console.log('Preset applied:', presetId)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy current inputs as preset JSON
|
||||
const copyCurrentPreset = async () => {
|
||||
if (!componentRef.value || !componentRef.value.inputs || !currentComponentInfo.value) {
|
||||
console.warn('No component selected or no inputs available')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Get current inputs value
|
||||
let inputsValue: unknown
|
||||
const inputsRef = componentRef.value.inputs as { value: unknown }
|
||||
if (typeof inputsRef === 'object' && inputsRef !== null && 'value' in inputsRef) {
|
||||
inputsValue = inputsRef.value
|
||||
} else {
|
||||
inputsValue = componentRef.value.inputs
|
||||
}
|
||||
|
||||
// Create preset template
|
||||
const presetTemplate = {
|
||||
type: 'preset',
|
||||
presetId: 'my-preset',
|
||||
presetName: 'my-preset',
|
||||
baseComponentCode: currentComponentInfo.value.code,
|
||||
description: `Preset for ${currentComponentInfo.value.name}`,
|
||||
version: '1.0.0',
|
||||
author: 'User',
|
||||
createdAt: new Date().toISOString(),
|
||||
inputs: inputsValue,
|
||||
metadata: {
|
||||
tags: ['custom'],
|
||||
category: 'user'
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to JSON string
|
||||
const jsonString = JSON.stringify(presetTemplate, null, 2)
|
||||
|
||||
// Copy to clipboard
|
||||
await navigator.clipboard.writeText(jsonString)
|
||||
|
||||
// Show notification (you might want to use Quasar's notification system)
|
||||
console.log('Preset copied to clipboard!')
|
||||
alert('Preset copied to clipboard! You can now save it as a .json file in the components_user/ directory.')
|
||||
} catch (err) {
|
||||
console.error('Failed to copy preset:', err)
|
||||
alert('Failed to copy preset to clipboard. See console for details.')
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
onMounted(() => {
|
||||
void fetchComponents()
|
||||
|
||||
Reference in New Issue
Block a user