Enhance Form Builder Components with Readonly State Support and New Modal Functionality
- Updated ComponentPreview.vue to include support for the 'searchSelect' and 'switch' components in readonly states, ensuring consistent behavior across form fields. - Modified FormBuilderComponents.vue to set default readonly properties for various components, enhancing usability and preventing unintended edits. - Enhanced FormBuilderFieldSettingsModal.vue to reflect the updated readonly logic for component types, improving user awareness of field capabilities. - Introduced a new modal in manage.vue for copying workflow links, allowing users to select between direct and iframe link types with customizable options. - Improved link generation logic to support iframe parameters, enhancing the flexibility of sharing workflows. - Updated styles in SearchSelect.vue and Switch.vue to visually indicate readonly states, ensuring a consistent user experience across components.
This commit is contained in:
parent
f024cc91dd
commit
f86fe87fc5
@ -27,7 +27,7 @@
|
|||||||
:label="component.props.label" :help="component.props.help" :placeholder="component.props.placeholder"
|
:label="component.props.label" :help="component.props.help" :placeholder="component.props.placeholder"
|
||||||
:validation="component.props.validation" :validation-visibility="isPreview ? 'live' : 'blur'"
|
:validation="component.props.validation" :validation-visibility="isPreview ? 'live' : 'blur'"
|
||||||
:readonly="component.props.readonly || !isPreview"
|
:readonly="component.props.readonly || !isPreview"
|
||||||
:disabled="!isPreview || (component.props.readonly && ['select', 'checkbox', 'radio'].includes(component.type))"
|
:disabled="!isPreview || (component.props.readonly && ['select', 'searchSelect', 'checkbox', 'radio', 'switch'].includes(component.type))"
|
||||||
:options="component.props.options || undefined" :value="component.props.value || undefined"
|
:options="component.props.options || undefined" :value="component.props.value || undefined"
|
||||||
:accept="component.props.accept || undefined" :max="component.props.max || undefined"
|
:accept="component.props.accept || undefined" :max="component.props.max || undefined"
|
||||||
:mask="component.props.mask || undefined" :digits="component.props.digits || undefined"
|
:mask="component.props.mask || undefined" :digits="component.props.digits || undefined"
|
||||||
@ -36,11 +36,12 @@
|
|||||||
:classes="component.type === 'checkbox' ? {
|
:classes="component.type === 'checkbox' ? {
|
||||||
wrapper: 'mb-1',
|
wrapper: 'mb-1',
|
||||||
options: 'space-y-0.5'
|
options: 'space-y-0.5'
|
||||||
} : {}" :class="{
|
} : { }" :class="{
|
||||||
'canvas-component': isPreview,
|
'canvas-component': isPreview,
|
||||||
'readonly-select': component.props.readonly && component.type === 'select',
|
'readonly-select': component.props.readonly && (component.type === 'select' || component.type === 'searchSelect'),
|
||||||
'readonly-checkbox': component.props.readonly && component.type === 'checkbox',
|
'readonly-checkbox': component.props.readonly && component.type === 'checkbox',
|
||||||
'readonly-radio': component.props.readonly && component.type === 'radio'
|
'readonly-radio': component.props.readonly && component.type === 'radio',
|
||||||
|
'readonly-switch': component.props.readonly && component.type === 'switch'
|
||||||
}" />
|
}" />
|
||||||
|
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
@ -1590,29 +1591,33 @@ const getButtonSizeClass = (size) => {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Readonly styles for select, checkbox, and radio components */
|
/* Readonly styles for select, checkbox, radio, and switch components */
|
||||||
:deep(.readonly-select),
|
:deep(.readonly-select),
|
||||||
:deep(.readonly-checkbox),
|
:deep(.readonly-checkbox),
|
||||||
:deep(.readonly-radio) {
|
:deep(.readonly-radio),
|
||||||
|
:deep(.readonly-switch) {
|
||||||
cursor: default !important;
|
cursor: default !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.readonly-select select),
|
:deep(.readonly-select select),
|
||||||
:deep(.readonly-select .formkit-inner),
|
:deep(.readonly-select .formkit-inner),
|
||||||
:deep(.readonly-checkbox input[type="checkbox"]),
|
:deep(.readonly-checkbox input[type="checkbox"]),
|
||||||
:deep(.readonly-radio input[type="radio"]) {
|
:deep(.readonly-radio input[type="radio"]),
|
||||||
|
:deep(.readonly-switch input[type="checkbox"]) {
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
opacity: 0.8 !important;
|
opacity: 0.8 !important;
|
||||||
background-color: #f3f4f6 !important;
|
background-color: #f3f4f6 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.readonly-checkbox) .formkit-options,
|
:deep(.readonly-checkbox) .formkit-options,
|
||||||
:deep(.readonly-radio) .formkit-options {
|
:deep(.readonly-radio) .formkit-options,
|
||||||
|
:deep(.readonly-switch) .formkit-wrapper {
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.readonly-checkbox) .formkit-wrapper,
|
:deep(.readonly-checkbox) .formkit-wrapper,
|
||||||
:deep(.readonly-radio) .formkit-wrapper {
|
:deep(.readonly-radio) .formkit-wrapper,
|
||||||
|
:deep(.readonly-switch) .formkit-wrapper {
|
||||||
cursor: not-allowed !important;
|
cursor: not-allowed !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,6 +136,7 @@ const availableComponents = [
|
|||||||
placeholder: 'Enter text...',
|
placeholder: 'Enter text...',
|
||||||
help: '',
|
help: '',
|
||||||
validation: '',
|
validation: '',
|
||||||
|
readonly: false,
|
||||||
// Conditional Logic Properties
|
// Conditional Logic Properties
|
||||||
conditionalLogic: {
|
conditionalLogic: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -156,6 +157,7 @@ const availableComponents = [
|
|||||||
placeholder: 'Enter text...',
|
placeholder: 'Enter text...',
|
||||||
help: '',
|
help: '',
|
||||||
validation: '',
|
validation: '',
|
||||||
|
readonly: false,
|
||||||
// Conditional Logic Properties
|
// Conditional Logic Properties
|
||||||
conditionalLogic: {
|
conditionalLogic: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -176,6 +178,7 @@ const availableComponents = [
|
|||||||
placeholder: '0',
|
placeholder: '0',
|
||||||
help: '',
|
help: '',
|
||||||
validation: '',
|
validation: '',
|
||||||
|
readonly: false,
|
||||||
// Conditional Logic Properties
|
// Conditional Logic Properties
|
||||||
conditionalLogic: {
|
conditionalLogic: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -196,6 +199,7 @@ const availableComponents = [
|
|||||||
placeholder: 'email@example.com',
|
placeholder: 'email@example.com',
|
||||||
help: '',
|
help: '',
|
||||||
validation: 'email',
|
validation: 'email',
|
||||||
|
readonly: false,
|
||||||
// Conditional Logic Properties
|
// Conditional Logic Properties
|
||||||
conditionalLogic: {
|
conditionalLogic: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -216,6 +220,7 @@ const availableComponents = [
|
|||||||
placeholder: 'Enter password...',
|
placeholder: 'Enter password...',
|
||||||
help: '',
|
help: '',
|
||||||
validation: '',
|
validation: '',
|
||||||
|
readonly: false,
|
||||||
// Conditional Logic Properties
|
// Conditional Logic Properties
|
||||||
conditionalLogic: {
|
conditionalLogic: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -235,7 +240,8 @@ const availableComponents = [
|
|||||||
type: 'url',
|
type: 'url',
|
||||||
placeholder: 'https://example.com',
|
placeholder: 'https://example.com',
|
||||||
help: '',
|
help: '',
|
||||||
validation: 'url'
|
validation: 'url',
|
||||||
|
readonly: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -248,7 +254,8 @@ const availableComponents = [
|
|||||||
type: 'tel',
|
type: 'tel',
|
||||||
placeholder: '+1 (555) 123-4567',
|
placeholder: '+1 (555) 123-4567',
|
||||||
help: '',
|
help: '',
|
||||||
validation: ''
|
validation: '',
|
||||||
|
readonly: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -262,7 +269,8 @@ const availableComponents = [
|
|||||||
placeholder: 'Enter value...',
|
placeholder: 'Enter value...',
|
||||||
help: 'Input will be formatted according to the mask',
|
help: 'Input will be formatted according to the mask',
|
||||||
mask: '###-###-####',
|
mask: '###-###-####',
|
||||||
validation: ''
|
validation: '',
|
||||||
|
readonly: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -295,6 +303,7 @@ const availableComponents = [
|
|||||||
{ label: 'Option 3', value: 'option_3' }
|
{ label: 'Option 3', value: 'option_3' }
|
||||||
],
|
],
|
||||||
validation: '',
|
validation: '',
|
||||||
|
readonly: false,
|
||||||
// Conditional Logic Properties
|
// Conditional Logic Properties
|
||||||
conditionalLogic: {
|
conditionalLogic: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -320,6 +329,7 @@ const availableComponents = [
|
|||||||
{ label: 'Option 3', value: 'option_3' }
|
{ label: 'Option 3', value: 'option_3' }
|
||||||
],
|
],
|
||||||
validation: '',
|
validation: '',
|
||||||
|
readonly: false,
|
||||||
// Conditional Logic Properties
|
// Conditional Logic Properties
|
||||||
conditionalLogic: {
|
conditionalLogic: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -345,7 +355,8 @@ const availableComponents = [
|
|||||||
{ label: 'Option 2', value: 'option_2' },
|
{ label: 'Option 2', value: 'option_2' },
|
||||||
{ label: 'Option 3', value: 'option_3' }
|
{ label: 'Option 3', value: 'option_3' }
|
||||||
],
|
],
|
||||||
validation: ''
|
validation: '',
|
||||||
|
readonly: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -362,7 +373,8 @@ const availableComponents = [
|
|||||||
{ label: 'Option 2', value: 'option_2' },
|
{ label: 'Option 2', value: 'option_2' },
|
||||||
{ label: 'Option 3', value: 'option_3' }
|
{ label: 'Option 3', value: 'option_3' }
|
||||||
],
|
],
|
||||||
validation: ''
|
validation: '',
|
||||||
|
readonly: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -377,7 +389,8 @@ const availableComponents = [
|
|||||||
name: 'switch_field',
|
name: 'switch_field',
|
||||||
help: 'Toggle this option on or off',
|
help: 'Toggle this option on or off',
|
||||||
value: false,
|
value: false,
|
||||||
validation: ''
|
validation: '',
|
||||||
|
readonly: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -2223,7 +2223,7 @@ const showField = (fieldName) => {
|
|||||||
rows: ['textarea'],
|
rows: ['textarea'],
|
||||||
options: ['select', 'searchSelect', 'checkbox', 'radio'],
|
options: ['select', 'searchSelect', 'checkbox', 'radio'],
|
||||||
conditionalLogic: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group'],
|
conditionalLogic: ['text', 'textarea', 'number', 'email', 'password', 'url', 'tel', 'select', 'searchSelect', 'checkbox', 'radio', 'switch', 'date', 'time', 'datetime-local', 'range', 'color', 'file', 'form-section', 'dynamic-list', 'repeating-table', 'repeating-group'],
|
||||||
readonly: ['text', 'number', 'email', 'textarea', 'mask', 'url', 'tel']
|
readonly: ['text', 'number', 'email', 'password', 'textarea', 'mask', 'url', 'tel', 'select', 'searchSelect', 'checkbox', 'radio', 'switch']
|
||||||
}
|
}
|
||||||
|
|
||||||
return fieldConfig[fieldName]?.includes(props.component.type) || false
|
return fieldConfig[fieldName]?.includes(props.component.type) || false
|
||||||
|
@ -5,14 +5,14 @@
|
|||||||
@input="handleChange"
|
@input="handleChange"
|
||||||
:options="context.options || []"
|
:options="context.options || []"
|
||||||
:placeholder="context.placeholder || 'Search and select an option'"
|
:placeholder="context.placeholder || 'Search and select an option'"
|
||||||
:disabled="context.disabled"
|
:disabled="context.disabled || context.readonly"
|
||||||
:searchable="true"
|
:searchable="!context.readonly"
|
||||||
:clearable="false"
|
:clearable="false"
|
||||||
label="label"
|
label="label"
|
||||||
:reduce="option => option.value"
|
:reduce="option => option.value"
|
||||||
:class="[
|
:class="[
|
||||||
'vue-select-wrapper',
|
'vue-select-wrapper',
|
||||||
{ 'vue-select-disabled': context.disabled }
|
{ 'vue-select-disabled': context.disabled || context.readonly }
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -21,7 +21,7 @@
|
|||||||
:value="_value"
|
:value="_value"
|
||||||
:name="context.node.name"
|
:name="context.node.name"
|
||||||
:id="context.id"
|
:id="context.id"
|
||||||
:disabled="context.disabled"
|
:disabled="context.disabled || context.readonly"
|
||||||
:required="context.attrs.required"
|
:required="context.attrs.required"
|
||||||
class="hidden-select"
|
class="hidden-select"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@ -60,6 +60,10 @@ const _value = computed({
|
|||||||
|
|
||||||
// Handle value changes from vue3-select-component
|
// Handle value changes from vue3-select-component
|
||||||
const handleChange = (value) => {
|
const handleChange = (value) => {
|
||||||
|
// Don't allow changes if readonly
|
||||||
|
if (props.context.readonly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
props.context.node.input(value)
|
props.context.node.input(value)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -81,4 +85,20 @@ const handleChange = (value) => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border-width: 0;
|
border-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Readonly styles for SearchSelect */
|
||||||
|
.vue-select-disabled {
|
||||||
|
opacity: 0.8;
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-select-disabled .vs__dropdown-toggle {
|
||||||
|
background-color: #f3f4f6 !important;
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-select-disabled .vs__actions {
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@ -4,6 +4,10 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function handleChange(event) {
|
function handleChange(event) {
|
||||||
|
// Don't allow changes if readonly
|
||||||
|
if (props.context.readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
props.context.node.input(event.target.checked);
|
props.context.node.input(event.target.checked);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -16,11 +20,11 @@ function handleChange(event) {
|
|||||||
:name="context.name"
|
:name="context.name"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="context.value"
|
:checked="context.value"
|
||||||
:disabled="context.disabled"
|
:disabled="context.disabled || context.readonly"
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
class="switch-input"
|
class="switch-input"
|
||||||
/>
|
/>
|
||||||
<label :for="context.id" class="switch-label">
|
<label :for="context.id" class="switch-label" :class="{ 'cursor-not-allowed': context.readonly }">
|
||||||
<span class="switch-track">
|
<span class="switch-track">
|
||||||
<span class="switch-thumb"></span>
|
<span class="switch-thumb"></span>
|
||||||
</span>
|
</span>
|
||||||
@ -91,4 +95,23 @@ function handleChange(event) {
|
|||||||
.switch-input:checked + .switch-label .switch-thumb {
|
.switch-input:checked + .switch-label .switch-thumb {
|
||||||
transform: translateX(1.25rem) translateZ(0);
|
transform: translateX(1.25rem) translateZ(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Readonly styles for Switch */
|
||||||
|
.switch-input:disabled + .switch-label .switch-track {
|
||||||
|
@apply opacity-50 cursor-not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-input:disabled + .switch-label {
|
||||||
|
@apply cursor-not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional readonly styling */
|
||||||
|
.switch-label.cursor-not-allowed {
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-label.cursor-not-allowed .switch-track {
|
||||||
|
opacity: 0.8;
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@ -29,6 +29,16 @@ const sortBy = ref('processCreatedDate');
|
|||||||
const sortOrder = ref('desc');
|
const sortOrder = ref('desc');
|
||||||
const currentView = ref('dashboard'); // 'dashboard', 'list', 'analytics'
|
const currentView = ref('dashboard'); // 'dashboard', 'list', 'analytics'
|
||||||
|
|
||||||
|
// Copy link modal state
|
||||||
|
const showCopyLinkModal = ref(false);
|
||||||
|
const selectedProcess = ref(null);
|
||||||
|
const copyLinkType = ref('direct'); // 'direct' or 'iframe'
|
||||||
|
const iframeOptions = ref({
|
||||||
|
debug: false,
|
||||||
|
hideComplete: true,
|
||||||
|
theme: 'light'
|
||||||
|
});
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
const itemsPerPage = ref(20); // Default to 20 items per page
|
const itemsPerPage = ref(20); // Default to 20 items per page
|
||||||
@ -451,16 +461,119 @@ onUnmounted(() => {
|
|||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy workflow run link to clipboard
|
// Enhanced copy workflow link functionality
|
||||||
const copyWorkflowLink = async (processId) => {
|
const openCopyLinkModal = (process) => {
|
||||||
|
selectedProcess.value = process;
|
||||||
|
showCopyLinkModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyWorkflowLink = async (processId, linkType = 'direct', options = {}) => {
|
||||||
try {
|
try {
|
||||||
const link = `${window.location.origin}/workflow/${processId}`;
|
// Check if we're on the client side
|
||||||
await navigator.clipboard.writeText(link);
|
if (process.client) {
|
||||||
toast.success('Run link copied to clipboard!');
|
let link = `${baseUrl.value}/workflow/${processId}`;
|
||||||
|
|
||||||
|
if (linkType === 'iframe') {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (options.debug !== undefined) params.append('debug', options.debug);
|
||||||
|
if (options.hideComplete !== undefined) params.append('hideComplete', options.hideComplete);
|
||||||
|
if (options.theme) params.append('theme', options.theme);
|
||||||
|
|
||||||
|
if (params.toString()) {
|
||||||
|
link += `?${params.toString()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(link);
|
||||||
|
toast.success(`${linkType === 'iframe' ? 'Iframe link' : 'Direct link'} copied to clipboard!`);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('Failed to copy link');
|
toast.error('Failed to copy link');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openWorkflowLink = (processId, linkType = 'direct', options = {}) => {
|
||||||
|
// Check if we're on the client side
|
||||||
|
if (process.client) {
|
||||||
|
let link = `${baseUrl.value}/workflow/${processId}`;
|
||||||
|
|
||||||
|
if (linkType === 'iframe') {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (options.debug !== undefined) params.append('debug', options.debug);
|
||||||
|
if (options.hideComplete !== undefined) params.append('hideComplete', options.hideComplete);
|
||||||
|
if (options.theme) params.append('theme', options.theme);
|
||||||
|
|
||||||
|
if (params.toString()) {
|
||||||
|
link += `?${params.toString()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.open(link, '_blank');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeCopyLinkModal = () => {
|
||||||
|
showCopyLinkModal.value = false;
|
||||||
|
selectedProcess.value = null;
|
||||||
|
copyLinkType.value = 'direct';
|
||||||
|
iframeOptions.value = {
|
||||||
|
debug: false,
|
||||||
|
hideComplete: true,
|
||||||
|
theme: 'light'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyLink = async () => {
|
||||||
|
if (!selectedProcess.value) return;
|
||||||
|
|
||||||
|
await copyWorkflowLink(selectedProcess.value.id, copyLinkType.value, copyLinkType.value === 'iframe' ? iframeOptions.value : {});
|
||||||
|
closeCopyLinkModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenLink = () => {
|
||||||
|
if (!selectedProcess.value) return;
|
||||||
|
|
||||||
|
openWorkflowLink(selectedProcess.value.id, copyLinkType.value, copyLinkType.value === 'iframe' ? iframeOptions.value : {});
|
||||||
|
closeCopyLinkModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Computed properties for link generation
|
||||||
|
const baseUrl = computed(() => {
|
||||||
|
return process.client ? window.location.origin : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const generatedLink = computed(() => {
|
||||||
|
if (!selectedProcess.value || !baseUrl.value) return '';
|
||||||
|
|
||||||
|
if (copyLinkType.value === 'iframe') {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (iframeOptions.value.debug !== undefined) params.append('debug', iframeOptions.value.debug);
|
||||||
|
if (iframeOptions.value.hideComplete !== undefined) params.append('hideComplete', iframeOptions.value.hideComplete);
|
||||||
|
if (iframeOptions.value.theme) params.append('theme', iframeOptions.value.theme);
|
||||||
|
|
||||||
|
return `${baseUrl.value}/workflow/${selectedProcess.value.id}${params.toString() ? `?${params.toString()}` : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${baseUrl.value}/workflow/${selectedProcess.value.id}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const generateIframeCode = () => {
|
||||||
|
if (!selectedProcess.value || !baseUrl.value) return '';
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (iframeOptions.value.debug !== undefined) params.append('debug', iframeOptions.value.debug);
|
||||||
|
if (iframeOptions.value.hideComplete !== undefined) params.append('hideComplete', iframeOptions.value.hideComplete);
|
||||||
|
if (iframeOptions.value.theme) params.append('theme', iframeOptions.value.theme);
|
||||||
|
|
||||||
|
const url = `${baseUrl.value}/workflow/${selectedProcess.value.id}${params.toString() ? `?${params.toString()}` : ''}`;
|
||||||
|
|
||||||
|
return `<iframe
|
||||||
|
src="${url}"
|
||||||
|
width="100%"
|
||||||
|
height="600px"
|
||||||
|
style="border: none; border-radius: 8px;">
|
||||||
|
</iframe>`;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -782,9 +895,9 @@ const copyWorkflowLink = async (processId) => {
|
|||||||
<!-- Copy Run Link Button (for published processes) -->
|
<!-- Copy Run Link Button (for published processes) -->
|
||||||
<button
|
<button
|
||||||
v-if="process.status === 'published'"
|
v-if="process.status === 'published'"
|
||||||
@click="copyWorkflowLink(process.id)"
|
@click="openCopyLinkModal(process)"
|
||||||
class="p-2 text-green-600 hover:text-green-800 hover:bg-green-50 rounded-lg transition-colors"
|
class="p-2 text-green-600 hover:text-green-800 hover:bg-green-50 rounded-lg transition-colors"
|
||||||
title="Copy Run Link"
|
title="Copy or Open Run Link"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
>
|
>
|
||||||
<Icon name="material-symbols:link" class="text-lg" />
|
<Icon name="material-symbols:link" class="text-lg" />
|
||||||
@ -1051,6 +1164,163 @@ const copyWorkflowLink = async (processId) => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</RsModal>
|
</RsModal>
|
||||||
|
|
||||||
|
<!-- Copy Link Modal -->
|
||||||
|
<RsModal v-model="showCopyLinkModal" title="Share Workflow Link" size="lg" position="center">
|
||||||
|
<div class="p-6">
|
||||||
|
<!-- Process Info -->
|
||||||
|
<div class="mb-6 p-4 bg-gray-50 rounded-lg">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<Icon name="material-symbols:link" class="text-blue-600 w-6 h-6" />
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-gray-900">{{ selectedProcess?.name }}</h3>
|
||||||
|
<p class="text-sm text-gray-600">{{ selectedProcess?.description || 'No description available' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Link Type Selection -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h4 class="font-medium text-gray-900 mb-3">Link Type</h4>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div
|
||||||
|
@click="copyLinkType = 'direct'"
|
||||||
|
:class="[
|
||||||
|
'p-4 border-2 rounded-lg cursor-pointer transition-colors',
|
||||||
|
copyLinkType === 'direct'
|
||||||
|
? 'border-blue-500 bg-blue-50'
|
||||||
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div :class="[
|
||||||
|
'w-4 h-4 rounded-full border-2',
|
||||||
|
copyLinkType === 'direct'
|
||||||
|
? 'border-blue-500 bg-blue-500'
|
||||||
|
: 'border-gray-300'
|
||||||
|
]">
|
||||||
|
<Icon v-if="copyLinkType === 'direct'" name="material-symbols:check" class="w-3 h-3 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 class="font-medium text-gray-900">Direct Link</h5>
|
||||||
|
<p class="text-sm text-gray-600">Direct URL to the workflow</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
@click="copyLinkType = 'iframe'"
|
||||||
|
:class="[
|
||||||
|
'p-4 border-2 rounded-lg cursor-pointer transition-colors',
|
||||||
|
copyLinkType === 'iframe'
|
||||||
|
? 'border-blue-500 bg-blue-50'
|
||||||
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div :class="[
|
||||||
|
'w-4 h-4 rounded-full border-2',
|
||||||
|
copyLinkType === 'iframe'
|
||||||
|
? 'border-blue-500 bg-blue-500'
|
||||||
|
: 'border-gray-300'
|
||||||
|
]">
|
||||||
|
<Icon v-if="copyLinkType === 'iframe'" name="material-symbols:check" class="w-3 h-3 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 class="font-medium text-gray-900">Iframe Integration</h5>
|
||||||
|
<p class="text-sm text-gray-600">Embed in external applications</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Iframe Options -->
|
||||||
|
<div v-if="copyLinkType === 'iframe'" class="mb-6">
|
||||||
|
<h4 class="font-medium text-gray-900 mb-3">Iframe Options</h4>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<label class="text-sm font-medium text-gray-700">Hide Debug Information</label>
|
||||||
|
<p class="text-xs text-gray-500">Remove debug UI elements</p>
|
||||||
|
</div>
|
||||||
|
<FormKit
|
||||||
|
v-model="iframeOptions.debug"
|
||||||
|
type="checkbox"
|
||||||
|
:classes="{
|
||||||
|
wrapper: 'flex items-center',
|
||||||
|
input: 'w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<label class="text-sm font-medium text-gray-700">Hide Completion Message</label>
|
||||||
|
<p class="text-xs text-gray-500">Auto-advance after completion</p>
|
||||||
|
</div>
|
||||||
|
<FormKit
|
||||||
|
v-model="iframeOptions.hideComplete"
|
||||||
|
type="checkbox"
|
||||||
|
:classes="{
|
||||||
|
wrapper: 'flex items-center',
|
||||||
|
input: 'w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-sm font-medium text-gray-700">Theme</label>
|
||||||
|
<FormKit
|
||||||
|
v-model="iframeOptions.theme"
|
||||||
|
type="select"
|
||||||
|
:options="[
|
||||||
|
{ label: 'Light', value: 'light' },
|
||||||
|
{ label: 'Dark', value: 'dark' }
|
||||||
|
]"
|
||||||
|
:classes="{
|
||||||
|
outer: 'mt-1',
|
||||||
|
input: 'w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Generated Link Preview -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h4 class="font-medium text-gray-900 mb-3">Generated Link</h4>
|
||||||
|
<div class="bg-gray-50 p-3 rounded-lg">
|
||||||
|
<code class="text-sm text-gray-800 break-all">
|
||||||
|
{{ generatedLink }}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Iframe Code Preview -->
|
||||||
|
<div v-if="copyLinkType === 'iframe'" class="mb-6">
|
||||||
|
<h4 class="font-medium text-gray-900 mb-3">Iframe Code</h4>
|
||||||
|
<div class="bg-gray-50 p-3 rounded-lg">
|
||||||
|
<pre class="text-sm text-gray-800 overflow-x-auto"><code>{{ generateIframeCode() }}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<RsButton @click="handleOpenLink" variant="secondary" :disabled="!selectedProcess">
|
||||||
|
<Icon name="material-symbols:open-in-new" class="mr-2" />
|
||||||
|
Open Link
|
||||||
|
</RsButton>
|
||||||
|
<RsButton @click="handleCopyLink" variant="primary" :disabled="!selectedProcess">
|
||||||
|
<Icon name="material-symbols:content-copy" class="mr-2" />
|
||||||
|
Copy Link
|
||||||
|
</RsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RsModal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user