| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499 |
- <template>
- <section id = "argument-popup">
- <div class="section-header">
- <h2>Start action: {{ title }}</h2>
- </div>
- <div class="section-content padding">
- <form @submit="handleSubmit">
- <template v-if="actionArguments.length > 0">
- <template v-for="arg in actionArguments" :key="arg.name">
- <label :for="arg.name">
- {{ formatLabel(arg.title) }}
- </label>
- <datalist v-if="(arg.suggestions && Object.keys(arg.suggestions).length > 0) || getBrowserSuggestions(arg).length > 0" :id="`${arg.name}-choices`">
- <option v-for="(suggestion, key) in arg.suggestions" :key="key" :value="key">
- {{ suggestion }}
- </option>
- <option v-for="(suggestion, index) in getBrowserSuggestions(arg)" :key="`browser-${index}`" :value="suggestion">
- {{ suggestion }}
- </option>
- </datalist>
- <select v-if="getInputComponent(arg) === 'select'" :id="arg.name" :name="arg.name" :value="getArgumentValue(arg)"
- :required="arg.required" @input="handleInput(arg, $event)" @change="handleChange(arg, $event)">
- <option v-for="choice in arg.choices" :key="choice.value" :value="choice.value">
- {{ choice.title || choice.value }}
- </option>
- </select>
-
- <component v-else :is="getInputComponent(arg)" :id="arg.name" :name="arg.name"
- :value="(arg.type === 'checkbox' || arg.type === 'confirmation') ? undefined : getArgumentValue(arg)"
- :checked="(arg.type === 'checkbox' || arg.type === 'confirmation') ? getArgumentValue(arg) : undefined"
- :list="(arg.suggestions || getBrowserSuggestions(arg).length > 0) ? `${arg.name}-choices` : undefined"
- :type="getInputComponent(arg) !== 'select' ? getInputType(arg) : undefined"
- :rows="arg.type === 'raw_string_multiline' ? 5 : undefined"
- :step="arg.type === 'datetime' ? 1 : undefined" :pattern="getPattern(arg)"
- @input="handleInput(arg, $event)" @change="handleChange(arg, $event)" />
- <span class="argument-description" v-html="arg.description"></span>
- </template>
- </template>
- <div v-else>
- <p>No arguments required</p>
- </div>
- <div class="buttons">
- <button name="start" type="submit" :disabled="hasConfirmation && !confirmationChecked">
- Start
- </button>
- <button name="cancel" type="button" @click="handleCancel">
- Cancel
- </button>
- </div>
- </form>
- </div>
- </section>
- </template>
- <script setup>
- import { ref, onMounted, nextTick } from 'vue'
- import { useRouter } from 'vue-router'
- const router = useRouter()
- // Reactive data
- const dialog = ref(null)
- const title = ref('')
- const icon = ref('')
- //const arguments = ref([])
- const argValues = ref({})
- const confirmationChecked = ref(false)
- const hasConfirmation = ref(false)
- const formErrors = ref({})
- const actionArguments = ref([])
- const popupOnStart = ref('')
- // Computed properties
- const props = defineProps({
- bindingId: {
- type: String,
- required: true
- }
- })
- // Methods
- async function setup() {
- const ret = await window.client.getActionBinding({
- bindingId: props.bindingId
- })
- const action = ret.action
- title.value = action.title
- icon.value = action.icon
- popupOnStart.value = action.popupOnStart || ''
- actionArguments.value = action.arguments || []
- argValues.value = {}
- formErrors.value = {}
- confirmationChecked.value = false
- hasConfirmation.value = false
- // Initialize values from query params or defaults
- actionArguments.value.forEach(arg => {
- if (arg.type === 'confirmation') {
- hasConfirmation.value = true
- const paramValue = getQueryParamValue(arg.name)
- let checkedValue = false
- if (paramValue !== null) {
- checkedValue = paramValue === '1' || paramValue === 'true' || paramValue === true
- } else if (arg.defaultValue !== undefined && arg.defaultValue !== '') {
- checkedValue = arg.defaultValue === '1' || arg.defaultValue === 'true' || arg.defaultValue === true
- }
- argValues.value[arg.name] = checkedValue
- confirmationChecked.value = checkedValue
- } else {
- const paramValue = getQueryParamValue(arg.name)
- if (arg.type === 'checkbox') {
- // For checkboxes, handle boolean default values properly
- if (paramValue !== null) {
- argValues.value[arg.name] = paramValue === '1' || paramValue === 'true' || paramValue === true
- } else if (arg.defaultValue !== undefined && arg.defaultValue !== '') {
- argValues.value[arg.name] = arg.defaultValue === '1' || arg.defaultValue === 'true' || arg.defaultValue === true
- } else {
- argValues.value[arg.name] = false
- }
- } else {
- argValues.value[arg.name] = paramValue !== null ? paramValue : arg.defaultValue || ''
- }
- }
- })
- // Run initial validation on all fields after DOM is updated
- await nextTick()
- for (const arg of actionArguments.value) {
- if (arg.type && !arg.type.startsWith('regex:') && arg.type !== 'select' && arg.type !== '' && arg.type !== 'confirmation' && arg.type !== 'checkbox') {
- await validateArgument(arg, argValues.value[arg.name] || '')
- }
- }
- }
- function getQueryParamValue(paramName) {
- const params = new URLSearchParams(window.location.search.substring(1))
- return params.get(paramName)
- }
- function formatLabel(title) {
- const lastChar = title.charAt(title.length - 1)
- if (lastChar === '?' || lastChar === '.' || lastChar === ':') {
- return title
- }
- return title + ':'
- }
- function getInputComponent(arg) {
- if (arg.type === 'html') {
- return 'div'
- } else if (arg.type === 'raw_string_multiline') {
- return 'textarea'
- } else if (arg.choices && arg.choices.length > 0 && (arg.type === 'select' || arg.type === '')) {
- return 'select'
- } else {
- return 'input'
- }
- }
- function getInputType(arg) {
- if (arg.type === 'html' || arg.type === 'raw_string_multiline' || arg.type === 'select') {
- return undefined
- }
- if (arg.type === 'confirmation') {
- return 'checkbox'
- }
- if (arg.type === 'ascii_identifier' || arg.type === 'ascii') {
- return 'text'
- }
- if (arg.type === 'datetime') {
- return 'datetime-local'
- }
- return arg.type
- }
- function getPattern(arg) {
- if (arg.type && arg.type.startsWith('regex:')) {
- return arg.type.replace('regex:', '')
- }
- return undefined
- }
- function getArgumentValue(arg) {
- if (arg.type === 'checkbox' || arg.type === 'confirmation') {
- return argValues.value[arg.name] === '1' || argValues.value[arg.name] === true || argValues.value[arg.name] === 'true'
- }
- return argValues.value[arg.name] || ''
- }
- function handleInput(arg, event) {
- const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value
- argValues.value[arg.name] = value
- updateUrlWithArg(arg.name, value)
- }
- function handleChange(arg, event) {
- if (arg.type === 'confirmation') {
- confirmationChecked.value = event.target.checked
- return
- }
- // Validate the input
- validateArgument(arg, event.target.value)
- }
- async function validateArgument(arg, value) {
- if (!arg.type || arg.type.startsWith('regex:')) {
- return
- }
- // Skip validation for datetime - backend will handle mangling values without seconds
- if (arg.type === 'datetime') {
- const inputElement = document.getElementById(arg.name)
- if (inputElement) {
- inputElement.setCustomValidity('')
- }
- delete formErrors.value[arg.name]
- return
- }
- // Skip validation for checkbox and confirmation - they're always valid
- if (arg.type === 'checkbox' || arg.type === 'confirmation') {
- const inputElement = document.getElementById(arg.name)
- if (inputElement) {
- inputElement.setCustomValidity('')
- }
- delete formErrors.value[arg.name]
- return
- }
- try {
- const validateArgumentTypeArgs = {
- value: value,
- type: arg.type,
- bindingId: props.bindingId,
- argumentName: arg.name
- }
- const validation = await window.client.validateArgumentType(validateArgumentTypeArgs)
- // Get the input element to set custom validity
- const inputElement = document.getElementById(arg.name)
-
- if (validation.valid) {
- delete formErrors.value[arg.name]
- // Clear custom validity message
- if (inputElement) {
- inputElement.setCustomValidity('')
- }
- } else {
- formErrors.value[arg.name] = validation.description
- // Set custom validity message
- if (inputElement) {
- inputElement.setCustomValidity(validation.description)
- }
- }
- } catch (err) {
- console.warn('Validation failed:', err)
- // On error, clear any custom validity
- const inputElement = document.getElementById(arg.name)
- if (inputElement) {
- inputElement.setCustomValidity('')
- }
- }
- }
- function updateUrlWithArg(name, value) {
- if (name && value !== undefined) {
- const url = new URL(window.location.href)
- // Don't add passwords to URL
- const arg = actionArguments.value.find(a => a.name === name)
- if (arg && arg.type === 'password') {
- return
- }
- url.searchParams.set(name, value)
- window.history.replaceState({}, '', url.toString())
- }
- }
- function getArgumentValues() {
- const ret = []
- for (const arg of actionArguments.value) {
- let value = argValues.value[arg.name] || ''
- if (arg.type === 'checkbox' || arg.type === 'confirmation') {
- value = value ? '1' : '0'
- }
- ret.push({
- name: arg.name,
- value: value
- })
- }
- return ret
- }
- function getUniqueId() {
- if (window.isSecureContext) {
- return window.crypto.randomUUID()
- } else {
- return Date.now().toString()
- }
- }
- function getBrowserSuggestions(arg) {
- if (!arg.suggestionsBrowserKey) {
- return []
- }
-
- try {
- const stored = localStorage.getItem(`olivetin-suggestions-${arg.suggestionsBrowserKey}`)
- if (stored) {
- const suggestions = JSON.parse(stored)
- return Array.isArray(suggestions) ? suggestions : []
- }
- } catch (err) {
- console.warn('Failed to load browser suggestions:', err)
- }
-
- return []
- }
- function saveBrowserSuggestions() {
- for (const arg of actionArguments.value) {
- if (arg.suggestionsBrowserKey) {
- const value = argValues.value[arg.name]
-
- // Only save non-empty values for non-checkbox/confirmation/password types
- if (value && value !== '' && arg.type !== 'checkbox' && arg.type !== 'confirmation' && arg.type !== 'password') {
- try {
- const key = `olivetin-suggestions-${arg.suggestionsBrowserKey}`
- const stored = localStorage.getItem(key)
- let suggestions = []
-
- if (stored) {
- suggestions = JSON.parse(stored)
- if (!Array.isArray(suggestions)) {
- suggestions = []
- }
- }
-
- // Add value if not already present
- if (!suggestions.includes(value)) {
- suggestions.unshift(value) // Add to beginning
- // Keep only the most recent 50 suggestions
- if (suggestions.length > 50) {
- suggestions = suggestions.slice(0, 50)
- }
- localStorage.setItem(key, JSON.stringify(suggestions))
- }
- } catch (err) {
- console.warn('Failed to save browser suggestions:', err)
- }
- }
- }
- }
- }
- async function startAction(actionArgs) {
- const startActionArgs = {
- bindingId: props.bindingId,
- arguments: actionArgs,
- uniqueTrackingId: getUniqueId()
- }
- try {
- const response = await window.client.startAction(startActionArgs)
- console.log('Action started successfully with tracking ID:', response.executionTrackingId)
- return response
- } catch (err) {
- console.error('Failed to start action:', err)
- throw err
- }
- }
- async function handleSubmit(event) {
- // Set custom validity for required fields
- for (const arg of actionArguments.value) {
- const value = argValues.value[arg.name]
- const inputElement = document.getElementById(arg.name)
-
- if (arg.required && (!value || value === '')) {
- formErrors.value[arg.name] = 'This field is required'
- // Set custom validity for required field validation
- if (inputElement) {
- inputElement.setCustomValidity('This field is required')
- }
- }
- }
- const form = event.target
- if (!form.checkValidity()) {
- console.log('argument form has elements that failed validation')
- return
- }
- event.preventDefault()
-
- const argvs = getArgumentValues()
- console.log('argument form has elements that passed validation')
-
- // Save values to localStorage for arguments with suggestionsBrowserKey
- saveBrowserSuggestions()
-
- try {
- const response = await startAction(argvs)
- if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
- router.push(`/logs/${response.executionTrackingId}`)
- } else {
- router.back()
- }
- } catch (err) {
- console.error('Failed to start action:', err)
- }
- }
- function handleCancel() {
- router.back()
- clearBookmark()
- }
- function clearBookmark() {
- window.history.replaceState({
- path: window.location.pathname
- }, '', window.location.pathname)
- }
- function show() {
- if (dialog.value) {
- dialog.value.showModal()
- }
- }
- function close() {
- if (dialog.value) {
- dialog.value.close()
- }
- }
- // Expose methods for parent components
- defineExpose({
- show,
- close
- })
- // Lifecycle
- onMounted(() => {
- setup()
- })
- </script>
- <style scoped>
- form {
- grid-template-columns: max-content auto auto;
- }
- .argument-description {
- font-size: 0.875rem;
- color: #666;
- margin-top: 0.25rem;
- }
- .buttons {
- display: flex;
- gap: 0.5rem;
- justify-content: flex-end;
- padding-top: 1rem;
- border-top: 1px solid #eee;
- }
- /* Checkbox specific styling */
- .argument-group input[type="checkbox"] {
- width: auto;
- margin-right: 0.5rem;
- }
- .argument-group input[type="checkbox"]+label {
- display: inline;
- font-weight: normal;
- }
- </style>
|