Major additions accumulated over 9 days — single commit per request. Flow editor (new): - Generic visual editor for step trees, usable by project wizard + agent flows - PROJECT_KINDS / AGENT_KINDS catalogs decouple UI from domain - Drag-and-drop reorder via vuedraggable with scope isolation per peer group - Chain-aware depends_on rewrite on reorder (sequential only — DAGs preserved) - Variable picker with per-applies_to catalog (Customer / Quotation / Service Contract / Issue / Subscription), insert + copy-clipboard modes - trigger_condition helper with domain-specific JSONLogic examples - Global FlowEditorDialog mounted once in MainLayout, Odoo inline pattern - Server: targo-hub flow-runtime.js, flow-api.js, flow-templates.js - ERPNext: Flow Template/Run doctypes, scheduler, 5 seeded system templates - depends_on chips resolve to step labels instead of opaque "s4" ids QR/OCR scanner (field app): - Camera capture → Gemini Vision via targo-hub with 8s timeout - IndexedDB offline queue retries photos when signal returns - Watcher merges late-arriving scan results into the live UI Dispatch: - Planning mode (draft → publish) with offer pool for unassigned jobs - Shared presets, recurrence selector, suggested-slots dialog - PublishScheduleModal, unassign confirmation Ops app: - ClientDetailPage composables extraction (useClientData, useDeviceStatus, useWifiDiagnostic, useModemDiagnostic) - Project wizard: shared detail sections, wizard catalog/publish composables - Address pricing composable + pricing-mock data - Settings redesign hosting flow templates Targo-hub: - Contract acceptance (JWT residential + DocuSeal commercial tracks) - Referral system - Modem-bridge diagnostic normalizer - Device extractors consolidated Migration scripts: - Invoice/quote print format setup, Jinja rendering - Additional import + fix scripts (reversals, dates, customers, payments) Docs: - Consolidated: old scattered MDs → HANDOFF, ARCHITECTURE, DATA_AND_FLOWS, FLOW_EDITOR_ARCHITECTURE, BILLING_AND_PAYMENTS, CPE_MANAGEMENT, APP_DESIGN_GUIDELINES - Archived legacy wizard PHP for reference - STATUS snapshots for 2026-04-18/19 Cleanup: - Removed ~40 generated PDFs/HTMLs (invoice_preview*, rendered_jinja*) - .gitignore now covers invoice preview output + nested .DS_Store Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
115 lines
4.0 KiB
Vue
115 lines
4.0 KiB
Vue
<!--
|
|
VariablePicker.vue — Menu de variables `{{path}}` cliquables.
|
|
|
|
Un dropdown qui liste toutes les variables disponibles pour un `applies_to`
|
|
donné (Customer / Quotation / Service Contract / Issue / Subscription).
|
|
|
|
Deux modes d'utilisation :
|
|
- Mode "insert" (défaut) : émet @insert('{{path}}') pour qu'un parent
|
|
l'ajoute à la fin du champ qu'il contrôle (ex : trigger_condition).
|
|
- Mode "copy" : copie `{{path}}` dans le presse-papier et
|
|
notifie l'utilisateur — utile quand il n'y a pas de champ cible
|
|
capturé dans le même composant (ex : dans le StepEditorModal où
|
|
n'importe quel champ de payload peut en avoir besoin).
|
|
|
|
Props :
|
|
- appliesTo : 'Customer' | 'Quotation' | ...
|
|
- label : texte du bouton (défaut "+ Variable")
|
|
- mode : 'insert' | 'copy' (défaut 'insert')
|
|
|
|
Events :
|
|
- insert(text) : émet `{{path}}` à insérer (mode 'insert' uniquement)
|
|
|
|
Performance :
|
|
- La liste est computed une fois par applies_to (mémoisée par Vue).
|
|
- Les entrées sont triées par domaine puis alphabétique pour rester stable.
|
|
-->
|
|
<template>
|
|
<q-btn-dropdown flat dense no-caps size="sm" :icon="icon" :label="label"
|
|
class="variable-picker" color="indigo-6" content-class="vp-menu">
|
|
<q-list dense class="vp-list">
|
|
<q-item-label header class="vp-header">
|
|
Variables disponibles
|
|
<span v-if="appliesTo" class="text-caption text-grey-6">
|
|
· {{ appliesTo }}
|
|
</span>
|
|
</q-item-label>
|
|
|
|
<q-item v-if="!appliesTo" class="vp-empty">
|
|
<q-item-section>
|
|
<q-item-label class="text-caption text-grey-7">
|
|
Sélectionnez « Applique à » pour voir les variables du domaine.
|
|
</q-item-label>
|
|
</q-item-section>
|
|
</q-item>
|
|
|
|
<q-item v-for="v in variables" :key="v.path" clickable v-close-popup
|
|
@click="onPick(v)" class="vp-item">
|
|
<q-item-section>
|
|
<q-item-label class="text-caption text-weight-medium">{{ v.label }}</q-item-label>
|
|
<q-item-label caption>
|
|
<code class="vp-code">{{ formatted(v.path) }}</code>
|
|
</q-item-label>
|
|
<q-item-label v-if="v.hint" caption class="text-grey-6 vp-hint">
|
|
{{ v.hint }}
|
|
</q-item-label>
|
|
</q-item-section>
|
|
<q-item-section side>
|
|
<q-icon :name="mode === 'copy' ? 'content_copy' : 'add_circle_outline'" size="16px" color="indigo-5" />
|
|
</q-item-section>
|
|
</q-item>
|
|
</q-list>
|
|
</q-btn-dropdown>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from 'vue'
|
|
import { Notify, copyToClipboard } from 'quasar'
|
|
import { getVariables } from './variables'
|
|
|
|
const props = defineProps({
|
|
appliesTo: { type: String, default: null },
|
|
label: { type: String, default: '+ Variable' },
|
|
mode: { type: String, default: 'insert' }, // 'insert' | 'copy'
|
|
icon: { type: String, default: 'data_object' },
|
|
})
|
|
|
|
const emit = defineEmits(['insert'])
|
|
|
|
const variables = computed(() => getVariables(props.appliesTo))
|
|
|
|
function formatted (path) { return `{{${path}}}` }
|
|
|
|
async function onPick (v) {
|
|
const text = formatted(v.path)
|
|
if (props.mode === 'copy') {
|
|
try {
|
|
await copyToClipboard(text)
|
|
Notify.create({ type: 'positive', message: `Copié : ${text}`, timeout: 1200, position: 'top' })
|
|
} catch {
|
|
Notify.create({ type: 'negative', message: 'Copie impossible', timeout: 1500 })
|
|
}
|
|
return
|
|
}
|
|
emit('insert', text)
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.variable-picker :deep(.q-btn__content) { font-size: 0.72rem; }
|
|
.vp-list { min-width: 320px; max-width: 380px; max-height: 420px; overflow-y: auto; }
|
|
.vp-header { font-weight: 600; font-size: 0.72rem; color: #475569; }
|
|
.vp-item { padding: 6px 12px; border-bottom: 1px solid #f1f5f9; }
|
|
.vp-item:last-child { border-bottom: none; }
|
|
.vp-code {
|
|
background: #f1f5f9;
|
|
color: #1e293b;
|
|
padding: 1px 5px;
|
|
border-radius: 3px;
|
|
font-size: 0.72rem;
|
|
font-family: ui-monospace, Menlo, monospace;
|
|
}
|
|
.vp-hint { font-size: 0.7rem; margin-top: 2px; }
|
|
.vp-empty { padding: 10px 14px; }
|
|
</style>
|