4daebf8c-dc42-4579-b104-0ed8afe17301 — Commit ac736fc9
Changed files
src/components/LaunchModal.vue | 125 ++++++++++++++++++++++------------------ src/types/index.ts | 6 ++ src/views/AppDetailView.vue | 44 ++++++++++++-- src/views/LaunchHistoryView.vue | 46 ++++++--------- 4 files changed, 133 insertions(+), 88 deletions(-)
Diff
diff --git a/src/components/LaunchModal.vue b/src/components/LaunchModal.vue
index c82e703..beedc95 100644
--- a/src/components/LaunchModal.vue
+++ b/src/components/LaunchModal.vue
@@ -1,12 +1,16 @@
<script setup lang="ts">
-import { ref, computed, watch } from 'vue'
-import type { DeeplinkTemplate, App, Environment } from '@/types'
-import { useHistoryStore } from '@/stores/history'
+import {ref, computed, watch} from 'vue'
+import type {DeeplinkTemplate, App, Environment} from '@/types'
+import {useHistoryStore} from '@/stores/history'
const props = defineProps<{
deeplink: DeeplinkTemplate
app: App
environment: Environment
+ orgId: string
+ initialPathValues?: Record<string, string>
+ initialQueryValues?: Record<string, string | boolean | string[]>
+ hideActions?: boolean
}>()
const emit = defineEmits<{
@@ -33,24 +37,27 @@ const queryValues = ref<Record<string, string | boolean | string[]>>({})
// Initialize states when deeplink changes
watch(
- () => props.deeplink,
- () => {
- pathValues.value = {}
- queryValues.value = {}
- for (const key of pathParams.value) {
- pathValues.value[key] = ''
- }
- for (const [key, type] of Object.entries(props.deeplink.queryParams)) {
- if (type === 'boolean') {
- queryValues.value[key] = false
- } else if (type === 'list') {
- queryValues.value[key] = ['']
- } else {
- queryValues.value[key] = ''
+ () => props.deeplink,
+ () => {
+ const initPath = props.initialPathValues
+ const initQuery = props.initialQueryValues
+ pathValues.value = {}
+ queryValues.value = {}
+ for (const key of pathParams.value) {
+ pathValues.value[key] = initPath?.[key] ?? ''
}
- }
- },
- { immediate: true }
+ for (const [key, type] of Object.entries(props.deeplink.queryParams)) {
+ const seed = initQuery?.[key]
+ if (type === 'boolean') {
+ queryValues.value[key] = typeof seed === 'boolean' ? seed : false
+ } else if (type === 'list') {
+ queryValues.value[key] = Array.isArray(seed) && seed.length > 0 ? [...seed] : ['']
+ } else {
+ queryValues.value[key] = typeof seed === 'string' ? seed : ''
+ }
+ }
+ },
+ {immediate: true}
)
function addListItem(key: string) {
@@ -70,7 +77,7 @@ function updateListItem(key: string, index: number, val: string) {
}
const unfilledParams = computed(() =>
- pathParams.value.filter(p => !pathValues.value[p]?.trim())
+ pathParams.value.filter(p => !pathValues.value[p]?.trim())
)
// Build URI
@@ -122,6 +129,12 @@ function launch() {
deeplinkName: props.deeplink.name,
uri,
environment: selectedEnv.value.name,
+ orgId: props.orgId,
+ deeplink: props.deeplink,
+ app: props.app,
+ environmentSnapshot: selectedEnv.value,
+ pathValues: {...pathValues.value},
+ queryValues: JSON.parse(JSON.stringify(queryValues.value)),
})
emit('close')
}
@@ -145,13 +158,13 @@ function onOverlayClick(e: MouseEvent) {
<div class="modal-subtitle" v-if="deeplink.description">{{ deeplink.description }}</div>
</div>
<div class="modal-header-actions">
- <button class="action-btn" title="Edit deeplink" @click="emit('edit', deeplink)">
+ <button v-if="!hideActions" class="action-btn" title="Edit deeplink" @click="emit('edit', deeplink)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
</button>
- <button class="action-btn action-btn-danger" title="Delete deeplink" @click="emit('delete', deeplink.id)">
+ <button v-if="!hideActions" class="action-btn action-btn-danger" title="Delete deeplink" @click="emit('delete', deeplink.id)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/>
@@ -169,15 +182,15 @@ function onOverlayClick(e: MouseEvent) {
<div class="section-label">Path Parameters</div>
<div class="params-list">
<div
- v-for="param in pathParams"
- :key="param"
- class="param-row"
+ v-for="param in pathParams"
+ :key="param"
+ class="param-row"
>
<label class="param-label">{{ param }}</label>
<input
- v-model="pathValues[param]"
- class="form-input"
- :placeholder="`Enter ${param}`"
+ v-model="pathValues[param]"
+ class="form-input"
+ :placeholder="`Enter ${param}`"
/>
</div>
</div>
@@ -185,14 +198,14 @@ function onOverlayClick(e: MouseEvent) {
<!-- Query params -->
<div
- v-if="Object.keys(deeplink.queryParams).length > 0"
- class="section"
+ v-if="Object.keys(deeplink.queryParams).length > 0"
+ class="section"
>
<div class="section-label">Query Parameters</div>
<div class="params-list">
<template
- v-for="[key, type] in Object.entries(deeplink.queryParams)"
- :key="key"
+ v-for="[key, type] in Object.entries(deeplink.queryParams)"
+ :key="key"
>
<!-- String -->
<div v-if="type === 'string'" class="param-row">
@@ -201,9 +214,9 @@ function onOverlayClick(e: MouseEvent) {
<span class="param-type">string</span>
</label>
<input
- v-model="(queryValues[key] as string)"
- class="form-input"
- :placeholder="`Enter ${key}`"
+ v-model="(queryValues[key] as string)"
+ class="form-input"
+ :placeholder="`Enter ${key}`"
/>
</div>
@@ -214,7 +227,7 @@ function onOverlayClick(e: MouseEvent) {
<span class="param-type">boolean</span>
</label>
<label class="toggle">
- <input type="checkbox" v-model="(queryValues[key] as boolean)" />
+ <input type="checkbox" v-model="(queryValues[key] as boolean)"/>
<span class="toggle-slider"></span>
</label>
</div>
@@ -227,32 +240,34 @@ function onOverlayClick(e: MouseEvent) {
</label>
<div class="list-items">
<div
- v-for="(item, idx) in (queryValues[key] as string[])"
- :key="idx"
- class="list-item-row"
+ v-for="(item, idx) in (queryValues[key] as string[])"
+ :key="idx"
+ class="list-item-row"
>
<input
- :value="item"
- class="form-input"
- :placeholder="`Item ${idx + 1}`"
- @input="updateListItem(key, idx, ($event.target as HTMLInputElement).value)"
+ :value="item"
+ class="form-input"
+ :placeholder="`Item ${idx + 1}`"
+ @input="updateListItem(key, idx, ($event.target as HTMLInputElement).value)"
/>
<button
- type="button"
- class="list-remove-btn"
- @click="removeListItem(key, idx)"
+ type="button"
+ class="list-remove-btn"
+ @click="removeListItem(key, idx)"
>
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+ stroke-width="2.5">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
</button>
</div>
<button
- type="button"
- class="add-list-btn"
- @click="addListItem(key)"
+ type="button"
+ class="add-list-btn"
+ @click="addListItem(key)"
>
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+ stroke-width="2.5">
<path d="M12 5v14M5 12h14"/>
</svg>
Add item
@@ -285,9 +300,9 @@ function onOverlayClick(e: MouseEvent) {
<div class="launch-actions">
<button class="btn btn-secondary" @click="emit('close')">Cancel</button>
<button
- class="btn btn-primary launch-btn"
- :disabled="unfilledParams.length > 0"
- @click="launch"
+ class="btn btn-primary launch-btn"
+ :disabled="unfilledParams.length > 0"
+ @click="launch"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M5 12h14M12 5l7 7-7 7"/>
diff --git a/src/types/index.ts b/src/types/index.ts
index 92fbd14..7958c25 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -36,4 +36,10 @@ export interface LaunchHistoryEntry {
deeplinkName: string
uri: string
environment: string
+ orgId?: string
+ deeplink?: DeeplinkTemplate
+ app?: App
+ environmentSnapshot?: Environment
+ pathValues?: Record<string, string>
+ queryValues?: Record<string, string | boolean | string[]>
}
diff --git a/src/views/AppDetailView.vue b/src/views/AppDetailView.vue
index b36424c..2c4f662 100644
--- a/src/views/AppDetailView.vue
+++ b/src/views/AppDetailView.vue
@@ -1,7 +1,8 @@
<script setup lang="ts">
-import { ref, onMounted, computed } from 'vue'
+import { ref, onMounted, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useOrganizationsStore } from '@/stores/organizations'
+import { useHistoryStore } from '@/stores/history'
import { getApp, getDeeplinks, deleteDeeplink } from '@/api/client'
import type { DeeplinkTemplate, App, Environment } from '@/types'
import AppLayout from '@/components/AppLayout.vue'
@@ -11,6 +12,7 @@ import LaunchModal from '@/components/LaunchModal.vue'
const route = useRoute()
const router = useRouter()
const orgStore = useOrganizationsStore()
+const historyStore = useHistoryStore()
const orgId = route.params.orgId as string
const appId = route.params.appId as string
@@ -21,10 +23,16 @@ const loading = ref(true)
const error = ref<string | null>(null)
const launchDeeplink = ref<DeeplinkTemplate | null>(null)
+const launchEnvOverride = ref<Environment | null>(null)
+const launchInitialPath = ref<Record<string, string> | undefined>(undefined)
+const launchInitialQuery = ref<Record<string, string | boolean | string[]> | undefined>(undefined)
const selectedEnvIndex = ref(0)
const selectedEnv = computed<Environment | null>(
() => app.value?.environments[selectedEnvIndex.value] ?? null
)
+const launchEnv = computed<Environment | null>(
+ () => launchEnvOverride.value ?? selectedEnv.value
+)
const org = computed(() => orgStore.organizations.find(o => o.id === orgId))
@@ -45,7 +53,29 @@ async function loadData() {
}
}
-onMounted(loadData)
+function maybeOpenFromHistory() {
+ const entryId = route.query.launchEntry as string | undefined
+ if (!entryId) return
+ const entry = historyStore.entries.find(e => e.id === entryId)
+ if (entry?.deeplink && entry.environmentSnapshot) {
+ launchDeeplink.value = entry.deeplink
+ launchEnvOverride.value = entry.environmentSnapshot
+ launchInitialPath.value = entry.pathValues
+ launchInitialQuery.value = entry.queryValues
+ const matchIdx = app.value?.environments.findIndex(e => e.name === entry.environmentSnapshot!.name) ?? -1
+ if (matchIdx >= 0) selectedEnvIndex.value = matchIdx
+ }
+ router.replace({query: {...route.query, launchEntry: undefined}})
+}
+
+onMounted(async () => {
+ await loadData()
+ maybeOpenFromHistory()
+})
+
+watch(() => route.query.launchEntry, (v) => {
+ if (v && !loading.value) maybeOpenFromHistory()
+})
async function handleDeleteDeeplink(deeplinkId: string) {
if (!confirm('Delete this deeplink? This cannot be undone.')) return
@@ -70,6 +100,9 @@ function handleLaunchDeeplink(deeplink: DeeplinkTemplate) {
function closeLaunchModal() {
launchDeeplink.value = null
+ launchEnvOverride.value = null
+ launchInitialPath.value = undefined
+ launchInitialQuery.value = undefined
}
</script>
@@ -171,10 +204,13 @@ function closeLaunchModal() {
<!-- Launch modal -->
<Teleport to="body">
<LaunchModal
- v-if="launchDeeplink && app && selectedEnv"
+ v-if="launchDeeplink && app && launchEnv"
:deeplink="launchDeeplink"
:app="app"
- :environment="selectedEnv"
+ :environment="launchEnv"
+ :orgId="orgId"
+ :initialPathValues="launchInitialPath"
+ :initialQueryValues="launchInitialQuery"
@close="closeLaunchModal"
@edit="dl => { closeLaunchModal(); handleEditDeeplink(dl) }"
@delete="id => { closeLaunchModal(); handleDeleteDeeplink(id) }"
diff --git a/src/views/LaunchHistoryView.vue b/src/views/LaunchHistoryView.vue
index 4fceafc..59fa3e4 100644
--- a/src/views/LaunchHistoryView.vue
+++ b/src/views/LaunchHistoryView.vue
@@ -1,10 +1,25 @@
<script setup lang="ts">
import {computed} from 'vue'
+import {useRouter} from 'vue-router'
import {useHistoryStore} from '@/stores/history'
import AppLayout from '@/components/AppLayout.vue'
+import type {LaunchHistoryEntry} from '@/types'
+const router = useRouter()
const historyStore = useHistoryStore()
+function openEntry(entry: LaunchHistoryEntry) {
+ if (entry.deeplink && entry.app && entry.environmentSnapshot && entry.orgId) {
+ router.push({
+ name: 'app-detail',
+ params: {orgId: entry.orgId, appId: entry.app.id},
+ query: {launchEntry: entry.id},
+ })
+ } else {
+ window.open(entry.uri)
+ }
+}
+
function formatDate(iso: string): string {
try {
return new Intl.DateTimeFormat(undefined, {
@@ -22,14 +37,6 @@ function clearHistory() {
}
}
-async function copyUri(uri: string) {
- await navigator.clipboard.writeText(uri)
-}
-
-function relaunch(uri: string) {
- window.open(uri)
-}
-
const grouped = computed(() => {
const groups: { date: string; entries: typeof historyStore.entries }[] = []
const seen: Record<string, number> = {}
@@ -86,6 +93,7 @@ const grouped = computed(() => {
v-for="entry in group.entries"
:key="entry.id"
class="history-entry"
+ @click="openEntry(entry)"
>
<div class="entry-main">
<div class="entry-header">
@@ -95,27 +103,6 @@ const grouped = computed(() => {
</div>
<div class="entry-uri">{{ entry.uri }}</div>
</div>
- <div class="entry-actions">
- <button
- class="entry-btn"
- title="Copy URI"
- @click="copyUri(entry.uri)"
- >
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
- <path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
- </svg>
- </button>
- <button
- class="entry-btn launch-btn"
- title="Re-launch"
- @click="relaunch(entry.uri)"
- >
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
- <path d="M5 12h14M12 5l7 7-7 7"/>
- </svg>
- </button>
- </div>
</div>
</div>
</div>
@@ -182,6 +169,7 @@ const grouped = computed(() => {
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 12px 14px;
+ cursor: pointer;
transition: border-color 0.12s;
}