the commit.
This commit is contained in:
Julien Nahum
2025-05-19 19:42:41 +02:00
44 changed files with 1858 additions and 801 deletions

View File

@@ -3,12 +3,12 @@
<template #label>
<slot name="label" />
</template>
<MentionDropdown
:state="mentionState"
:mention-state="mentionState"
:mentions="mentions"
/>
<div class="relative">
<div
ref="editableDiv"
@@ -38,14 +38,14 @@
@click="openMentionDropdown"
/>
</div>
<template
v-if="$slots.help"
#help
>
<slot name="help" />
</template>
<template
v-if="$slots.error"
#error
@@ -54,146 +54,169 @@
</template>
</InputWrapper>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { inputProps, useFormInput } from './useFormInput.js'
import InputWrapper from './components/InputWrapper.vue'
import MentionDropdown from './components/MentionDropdown.vue'
const props = defineProps({
...inputProps,
mentions: { type: Array, default: () => [] },
disableMention: { type: Boolean, default: false },
})
const emit = defineEmits(['update:modelValue'])
const { compVal, inputStyle, hasError, inputWrapperProps } = useFormInput(props, { emit })
const editableDiv = ref(null)
const savedRange = ref(null)
const subscriptionModalStore = useSubscriptionModalStore()
const mentionState = ref({
open: false,
onInsert: (mention) => {
insertMention(mention)
mentionState.value.open = false
},
onCancel: () => {
mentionState.value.open = false
restoreSelection()
},
})
const createMentionSpan = (mention) => {
const mentionSpan = document.createElement('span')
mentionSpan.setAttribute('mention-field-id', mention.field.id)
mentionSpan.setAttribute('mention-field-name', mention.field.name)
mentionSpan.setAttribute('mention-fallback', mention.fallback || '')
mentionSpan.setAttribute('contenteditable', 'false')
mentionSpan.setAttribute('mention', 'true')
mentionSpan.textContent = mention.field.name.length > 25 ? `${mention.field.name.slice(0, 25)}...` : mention.field.name
return mentionSpan
}
const insertMention = (mention) => {
const mentionSpan = createMentionSpan(mention)
<script setup>
import { ref, onMounted, watch, reactive } from 'vue'
import { inputProps, useFormInput } from './useFormInput.js'
import InputWrapper from './components/InputWrapper.vue'
import MentionDropdown from './components/MentionDropdown.vue'
const props = defineProps({
...inputProps,
mentions: { type: Array, default: () => [] },
disableMention: { type: Boolean, default: false },
})
const emit = defineEmits(['update:modelValue'])
const { compVal, inputStyle, hasError, inputWrapperProps } = useFormInput(props, { emit })
const editableDiv = ref(null)
const savedRange = ref(null)
const subscriptionModalStore = useSubscriptionModalStore()
// Create a reactive state object for the mention dropdown
const mentionState = reactive({
open: false,
onInsert: (mention) => {
insertMention(mention)
mentionState.open = false
},
onCancel: () => {
mentionState.open = false
restoreSelection()
const range = window.getSelection().getRangeAt(0)
// Insert the mention span
range.insertNode(mentionSpan)
// Move the cursor after the inserted mention
range.setStartAfter(mentionSpan)
range.collapse(true)
// Apply the new selection
},
})
const createMentionSpan = (mention) => {
const mentionSpan = document.createElement('span')
mentionSpan.setAttribute('mention', 'true')
mentionSpan.setAttribute('mention-field-id', mention.field.id)
mentionSpan.setAttribute('mention-field-name', mention.field.name)
mentionSpan.setAttribute('mention-fallback', mention.fallback || '')
mentionSpan.setAttribute('contenteditable', 'false')
mentionSpan.setAttribute('class', 'mention-item')
mentionSpan.textContent = mention.field.name.length > 25 ? `${mention.field.name.slice(0, 25)}...` : mention.field.name
return mentionSpan
}
const insertMention = (mention) => {
const mentionSpan = createMentionSpan(mention)
restoreSelection()
const range = window.getSelection().getRangeAt(0)
// Insert the mention span
range.insertNode(mentionSpan)
// Move the cursor after the inserted mention
range.setStartAfter(mentionSpan)
range.collapse(true)
// Apply the new selection
const selection = window.getSelection()
selection.removeAllRanges()
selection.addRange(range)
// Ensure the editableDiv is focused
editableDiv.value.focus()
updateCompVal()
}
const openMentionDropdown = () => {
if (props.disableMention) {
subscriptionModalStore.setModalContent('Upgrade to Pro', 'Upgrade to Pro to use mentions')
subscriptionModalStore.openModal()
return
}
saveSelection()
if (!savedRange.value) {
// If no previous selection, move cursor to the end
const range = document.createRange()
range.selectNodeContents(editableDiv.value)
range.collapse(false)
const selection = window.getSelection()
selection.removeAllRanges()
selection.addRange(range)
// Ensure the editableDiv is focused
editableDiv.value.focus()
updateCompVal()
savedRange.value = range
}
const openMentionDropdown = () => {
if (props.disableMention) {
subscriptionModalStore.setModalContent('Upgrade to Pro', 'Upgrade to Pro to use mentions')
subscriptionModalStore.openModal()
return
}
saveSelection()
if (!savedRange.value) {
// If no previous selection, move cursor to the end
const range = document.createRange()
range.selectNodeContents(editableDiv.value)
range.collapse(false)
const selection = window.getSelection()
selection.removeAllRanges()
selection.addRange(range)
savedRange.value = range
}
mentionState.value.open = true
mentionState.open = true
}
const saveSelection = () => {
const selection = window.getSelection()
if (selection.rangeCount > 0) {
savedRange.value = selection.getRangeAt(0)
}
const saveSelection = () => {
}
const restoreSelection = () => {
if (savedRange.value) {
const selection = window.getSelection()
if (selection.rangeCount > 0) {
savedRange.value = selection.getRangeAt(0)
}
selection.removeAllRanges()
selection.addRange(savedRange.value)
editableDiv.value.focus()
}
const restoreSelection = () => {
if (savedRange.value) {
const selection = window.getSelection()
selection.removeAllRanges()
selection.addRange(savedRange.value)
editableDiv.value.focus()
}
}
const updateCompVal = () => {
compVal.value = editableDiv.value.innerHTML
}
const onInput = () => {
updateCompVal()
}
onMounted(() => {
if (compVal.value) {
editableDiv.value.innerHTML = compVal.value
}
const updateCompVal = () => {
compVal.value = editableDiv.value.innerHTML
})
watch(compVal, (newVal) => {
if (editableDiv.value && editableDiv.value.innerHTML !== newVal) {
editableDiv.value.innerHTML = newVal
}
const onInput = () => {
updateCompVal()
}
onMounted(() => {
if (compVal.value) {
editableDiv.value.innerHTML = compVal.value
}
})
watch(compVal, (newVal) => {
if (editableDiv.value && editableDiv.value.innerHTML !== newVal) {
editableDiv.value.innerHTML = newVal
}
})
defineExpose({
editableDiv,
compVal,
mentionState,
openMentionDropdown,
onInput,
})
</script>
<style scoped>
.mention-input {
min-height: 1.5rem;
white-space: pre-wrap;
word-break: break-word;
}
.mention-input:empty::before {
content: attr(placeholder);
color: #9ca3af;
}
.mention-input span[mention] {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-flex;
align-items: center;
background-color: #dbeafe;
color: #1e40af;
border: 1px solid #bfdbfe;
border-radius: 0.25rem;
padding: 0 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
position: relative;
vertical-align: baseline;
}
</style>
})
defineExpose({
editableDiv,
compVal,
mentionState,
openMentionDropdown,
onInput,
})
</script>
<style scoped>
.mention-input {
min-height: 1.5rem;
white-space: pre-wrap;
word-break: break-word;
}
.mention-input:empty::before {
content: attr(placeholder);
color: #9ca3af;
}
.mention-input span[mention] {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-flex;
align-items: center;
background-color: #dbeafe;
color: #1e40af;
border: 1px solid #bfdbfe;
border-radius: 0.25rem;
padding: 0 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
position: relative;
vertical-align: baseline;
}
</style>

View File

@@ -31,6 +31,7 @@
:options="quillOptions"
:disabled="disabled"
:style="inputStyle"
@ready="onEditorReady"
/>
</div>
@@ -59,7 +60,7 @@
<MentionDropdown
v-if="enableMentions && mentionState"
:state="mentionState"
:mention-state="mentionState"
:mentions="mentions"
/>
</InputWrapper>
@@ -103,12 +104,23 @@ watch(compVal, (val) => {
}
}, { immediate: true })
// Move the mention extension registration to onMounted
if (props.enableMentions && !Quill.imports['blots/mention']) {
// Initialize mention extension
if (props.enableMentions) {
// Register the mention extension with Quill
mentionState.value = registerMentionExtension(Quill)
}
// Handle editor ready event
const onEditorReady = (quillInstance) => {
// If we have a mention module, get its state
if (props.enableMentions && quillInstance) {
const mentionModule = quillInstance.getModule('mention')
if (mentionModule && mentionModule.state) {
mentionState.value = mentionModule.state
}
}
}
const quillOptions = computed(() => {
const defaultOptions = {
placeholder: props.placeholder || '',

View File

@@ -1,7 +1,7 @@
<template>
<UPopover
ref="popover"
v-model:open="open"
v-model:open="mentionState.open"
class="h-0"
@close="cancel"
>
@@ -43,7 +43,7 @@
</div>
</div>
</div>
<div class="flex border-t pt-2 -mx-2 px-2 justify-end space-x-2">
<UButton
size="sm"
@@ -66,51 +66,62 @@
</template>
</UPopover>
</template>
<script setup>
import { ref, toRefs } from 'vue'
import BlockTypeIcon from '~/components/open/forms/components/BlockTypeIcon.vue'
import blocksTypes from '~/data/blocks_types.json'
const props = defineProps({
state: Object,
mentions: Array
})
defineShortcuts({
escape: () => {
open.value = false
}
})
const { open, onInsert, onCancel } = toRefs(props.state)
const selectedField = ref(null)
const fallbackValue = ref('')
const filteredMentions = computed(() => {
return props.mentions.filter(mention => blocksTypes[mention.type]?.is_input ?? false)
})
function selectField(field, insert = false) {
selectedField.value = {...field}
if (insert) {
insertMention()
}
<script setup>
import { ref, computed, watch } from 'vue'
import BlockTypeIcon from '~/components/open/forms/components/BlockTypeIcon.vue'
import blocksTypes from '~/data/blocks_types.json'
const props = defineProps({
mentionState: {
type: Object,
required: true
},
mentions: {
type: Array,
required: true
}
watch(open, (newValue) => {
if (newValue) {
selectedField.value = null
fallbackValue.value = ''
}
})
const insertMention = () => {
if (selectedField.value && onInsert.value) {
onInsert.value({
field: selectedField.value,
fallback: fallbackValue.value
})
open.value = false
}
})
defineShortcuts({
escape: () => {
props.mentionState.open = false
}
const cancel = () => {
if (onCancel.value) {
onCancel.value()
}
open.value = false
})
const selectedField = ref(null)
const fallbackValue = ref('')
const filteredMentions = computed(() => {
return props.mentions.filter(mention => blocksTypes[mention.type]?.is_input ?? false)
})
function selectField(field, insert = false) {
selectedField.value = {...field}
if (insert) {
insertMention()
}
</script>
}
watch(() => props.mentionState.open, (newValue) => {
if (newValue) {
selectedField.value = null
fallbackValue.value = ''
}
})
const insertMention = () => {
if (selectedField.value && props.mentionState.onInsert) {
props.mentionState.onInsert({
field: selectedField.value,
fallback: fallbackValue.value
})
}
}
const cancel = () => {
if (props.mentionState.onCancel) {
props.mentionState.onCancel()
}
}
</script>

View File

@@ -8,6 +8,7 @@
<script setup>
import Quill from 'quill'
import 'quill/dist/quill.snow.css'
import '../../../lib/quill/quillPatches'
import { onMounted, onBeforeUnmount, ref, watch } from 'vue'
const props = defineProps({
@@ -35,13 +36,40 @@ let quillInstance = null
const container = ref(null)
const model = ref(props.modelValue)
// Safely paste HTML content with handling for empty content
const pasteHTML = (instance) => {
instance.clipboard.dangerouslyPasteHTML(props.modelValue || '', 'silent')
if (!props.modelValue) {
instance.setContents([])
return
}
try {
instance.clipboard.dangerouslyPasteHTML(props.modelValue, 'silent')
} catch (error) {
console.error('Error pasting HTML:', error)
// Fallback to setting empty content
instance.setContents([])
}
}
const initializeQuill = () => {
if (container.value) {
quillInstance = new Quill(container.value, props.options)
// Merge default options with user options
const defaultOptions = {
formats: ['bold', 'color', 'font', 'code', 'italic', 'link', 'size', 'strike', 'script', 'underline', 'header', 'list', 'mention']
}
const mergedOptions = {
...defaultOptions,
...props.options,
modules: {
...defaultOptions.modules,
...(props.options.modules || {})
}
}
// Initialize Quill with merged options
quillInstance = new Quill(container.value, mergedOptions)
quillInstance.on('selection-change', (range, oldRange, source) => {
if (!range) {

View File

@@ -1,7 +1,18 @@
<template>
<notion-renderer
<NotionRenderer
v-if="!loading"
:block-map="blockMap"
:block-map="blocks"
:block-overrides="blockOverrides"
:content-id="contentId"
:full-page="fullPage"
:hide-list="hideList"
:level="level"
:map-image-url="mapImageUrl"
:map-page-url="mapPageUrl"
:page-link-options="pageLinkOptions"
:image-options="imageOptions"
:prism="prism"
:todo="todo"
/>
<div
v-else
@@ -12,27 +23,69 @@
</template>
<script>
import { NotionRenderer } from "vue-notion"
import { NotionRenderer, defaultMapPageUrl, defaultMapImageUrl } from 'vue-notion'
export default {
name: "NotionPage",
name: 'NotionPage',
components: { NotionRenderer },
props: {
blockMap: {
type: Object
},
blockOverrides: {
type: Object,
default: () => ({})
},
loading: {
type: Boolean,
required: true,
type: Boolean
},
contentId: String,
contentIndex: { type: Number, default: 0 },
fullPage: { type: Boolean, default: false },
hideList: { type: Array, default: () => [] },
level: { type: Number, default: 0 },
mapImageUrl: { type: Function, default: defaultMapImageUrl },
mapPageUrl: { type: Function, default: defaultMapPageUrl },
pageLinkOptions: {
type: Object, default: () => {
const NuxtLink = resolveComponent('NuxtLink')
return { component: NuxtLink, href: 'to' }
}
},
imageOptions: Object,
prism: { type: Boolean, default: false },
todo: { type: Boolean, default: false }
},
computed: {
blocks () {
if (this.blockMap && this.blockMap.data) {
return this.blockMap.data
}
return this.blockMap
}
}
}
</script>
<style lang="scss">
<style lang='scss'>
@import "vue-notion/src/styles.css";
.notion-blue {
@apply text-nt-blue;
}
.notion-spacer {
width: 24px !important;
}
.notion-link {
text-decoration: none;
}
.notion {
img, iframe {
@apply rounded-md;
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div
v-show="isActive"
v-if="isActive"
class="settings-section"
>
<h3 class="text-xl font-medium mb-1">

View File

@@ -5,19 +5,16 @@
@close="emit('close')"
>
<open-form
v-if="form"
:form-manager="formManager"
:theme="theme"
:loading="false"
:form="form"
:fields="form.properties"
:default-data-form="submission"
:mode="FormMode.EDIT"
@submit="updateForm"
>
<template #submit-btn="{ submitForm }">
<template #submit-btn="{ loading }">
<v-button
:loading="loading"
class="mt-2 px-8 mx-1"
@click.prevent="submitForm"
@click.prevent="updateForm"
>
Update Submission
</v-button>
@@ -25,11 +22,13 @@
</open-form>
</modal>
</template>
<script setup>
import { ref, defineProps, defineEmits } from "vue"
import OpenForm from "../forms/OpenForm.vue"
import CachedDefaultTheme from "~/lib/forms/themes/CachedDefaultTheme.js"
import { FormMode } from "~/lib/forms/FormModeStrategy.js"
import { useFormManager } from '~/lib/forms/composables/useFormManager'
const props = defineProps({
show: { type: Boolean, required: true },
@@ -46,13 +45,37 @@ const props = defineProps({
submission: { type: Object },
})
// Set up form manager with proper mode
let formManager = null
const setupFormManager = () => {
if (!props.form) return null
formManager = useFormManager(props.form, FormMode.EDIT)
return formManager
}
// Initialize form manager
formManager = setupFormManager()
watch(() => props.show, (newShow) => {
if (newShow) {
nextTick(() => {
formManager.initialize({
skipPendingSubmission: true,
skipUrlParams: true,
defaultData: props.submission
})
})
}
})
const loading = ref(false)
const emit = defineEmits(["close", "updated"])
const updateForm = (form, onFailure) => {
const updateForm = () => {
loading.value = true
form
.put("/open/forms/" + props.form.id + "/submissions/" + props.submission.id)
formManager.form.put("/open/forms/" + props.form.id + "/submissions/" + props.submission.id)
.then((res) => {
useAlert().success(res.message)
loading.value = false
@@ -65,7 +88,6 @@ const updateForm = (form, onFailure) => {
useAlert().formValidationError(error.data)
}
loading.value = false
onFailure()
})
}
</script>

View File

@@ -4,7 +4,7 @@
orientation="horizontal"
>
<UButton
v-track.delete_record_click
v-track.edit_record_click
size="sm"
color="white"
icon="heroicons:pencil-square"

View File

@@ -55,7 +55,7 @@
class="m-2 my-4">
<UAlert
:close-button="{ icon: 'i-heroicons-x-mark-20-solid', color: 'gray', variant: 'link', padded: false }"
color="yellow"
color="amber"
variant="subtle"
icon="i-material-symbols-info-outline"
@close="hidePasswordDisabledMsg = true"
@@ -64,29 +64,35 @@
</div>
<div
<UAlert
v-if="isPublicFormPage && (form.is_closed || form.visibility=='closed')"
class="border shadow-sm p-2 my-4 flex items-center rounded-md bg-yellow-100 dark:bg-yellow-600/20 border-yellow-500 dark:border-yellow-500/20"
icon="i-heroicons-lock-closed-20-solid"
color="amber"
variant="subtle"
class="m-2 my-4"
>
<div class="flex-grow">
<template #description>
<div
class="mb-0 py-2 px-4 text-yellow-600"
class="py-2"
v-html="form.closed_text"
/>
</div>
</div>
</template>
</UAlert>
<div
<UAlert
v-else-if="isPublicFormPage && form.max_number_of_submissions_reached"
class="border shadow-sm p-2 my-4 flex items-center rounded-md bg-yellow-100 dark:bg-yellow-600/20 border-yellow-500 dark:border-yellow-500/20"
icon="i-heroicons-lock-closed-20-solid"
color="amber"
variant="subtle"
class="m-2 my-4"
>
<div class="flex-grow">
<template #description>
<div
class="mb-0 py-2 px-4 text-yellow-600 dark:text-yellow-600"
class="py-2"
v-html="form.max_submissions_reached_text"
/>
</div>
</div>
</template>
</UAlert>
<form-cleanings
v-if="showFormCleanings"
@@ -343,7 +349,7 @@ const triggerSubmit = async () => {
submissionId: submissionId.value
}).then(result => {
if (result) {
submittedData.value = result || {}
submittedData.value = formManager.form.data()
if (result?.submission_id) {
submissionId.value = result.submission_id
@@ -392,7 +398,9 @@ const addPasswordError = (msg) => {
}
defineExpose({
addPasswordError
addPasswordError,
restart,
formManager
})
</script>

View File

@@ -65,7 +65,7 @@
<slot
v-if="isLastPage"
name="submit-btn"
:loading="form.busy"
:loading="isProcessing"
/>
<open-form-button
v-else-if="currentFieldsPageBreak"
@@ -73,7 +73,7 @@
:color="form.color"
:theme="theme"
class="mt-2 px-8 mx-1"
:loading="form.busy"
:loading="isProcessing"
@click.stop="handleNextClick"
>
{{ currentFieldsPageBreak.next_btn_text }}
@@ -165,6 +165,8 @@ const handleDragDropped = (data) => {
workingFormStore.moveField(oldTargetIndex, newTargetIndex)
}
}
const isProcessing = computed(() => props.formManager.state.isProcessing)
</script>
<style lang='scss' scoped>

View File

@@ -23,7 +23,7 @@
]"
/>
<div class="flex-grow flex justify-center">
<div class="flex-grow flex justify-center gap-2">
<EditableTag
id="form-editor-title"
v-model="form.title"
@@ -34,12 +34,14 @@
v-if="form.visibility == 'draft'"
color="yellow"
variant="soft"
icon="i-heroicons-pencil-square"
label="Draft"
/>
<UBadge
v-else-if="form.visibility == 'closed'"
color="gray"
variant="soft"
icon="i-heroicons-lock-closed-20-solid"
label="Closed"
/>
</div>

View File

@@ -0,0 +1,110 @@
<template>
<div
v-if="shouldDisplayBadges"
class="flex items-center flex-wrap gap-1"
>
<!-- Draft Badge -->
<UTooltip v-if="form.visibility === 'draft'" text="Not publicly accessible">
<UBadge
color="amber"
variant="subtle"
icon="i-heroicons-exclamation-triangle"
:size="size"
>
Draft
</UBadge>
</UTooltip>
<!-- Closed Badge -->
<UTooltip v-else-if="form.visibility === 'closed'" text="Won't accept new submissions">
<UBadge
color="gray"
variant="subtle"
icon="i-heroicons-lock-closed"
:size="size"
>
Closed
</UBadge>
</UTooltip>
<!-- Time Limited Badge -->
<UTooltip v-if="form.closes_at && !form.is_closed" :text="`Will close on ${closesDate}`">
<UBadge
color="amber"
variant="subtle"
icon="i-heroicons-clock"
:size="size"
>
Time limited
</UBadge>
</UTooltip>
<!-- Submission Limited Badge -->
<UTooltip
v-if="form.max_submissions_count > 0 && !form.max_number_of_submissions_reached"
:text="`Limited to ${form.max_submissions_count} submissions`"
>
<UBadge
color="amber"
variant="subtle"
icon="i-heroicons-chart-bar"
:size="size"
>
Submission limited
</UBadge>
</UTooltip>
<!-- Tags Badges -->
<UBadge
v-for="tag in form.tags"
:key="tag"
color="white"
variant="solid"
class="capitalize"
:size="size"
>
{{ tag }}
</UBadge>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
form: {
type: Object,
required: true
},
size: {
type: String,
default: 'sm',
validator: (value) => ['xs', 'sm', 'md', 'lg'].includes(value)
}
})
const closesDate = computed(() => {
if (props.form && props.form.closes_at) {
try {
const dateObj = new Date(props.form.closes_at)
return dateObj.getFullYear() + '-' +
String(dateObj.getMonth() + 1).padStart(2, '0') + '-' +
String(dateObj.getDate()).padStart(2, '0') + ' ' +
String(dateObj.getHours()).padStart(2, '0') + ':' +
String(dateObj.getMinutes()).padStart(2, '0')
} catch (e) {
console.error(e)
return null
}
}
return null
})
// Conditional to determine if badges should be displayed
const shouldDisplayBadges = computed(() => {
return ['draft', 'closed'].includes(props.form.visibility) ||
(props.form.tags && props.form.tags.length > 0) ||
props.form.closes_at ||
(props.form.max_submissions_count > 0)
})
</script>

View File

@@ -142,6 +142,13 @@ watch(() => form.value.dark_mode, () => {
handleDarkModeChange()
})
// Watch for form mode changes to reset the form when switching modes
watch(formMode, () => {
if (previewFormSubmitted.value) {
restartForm()
}
})
onMounted(() => {
handleDarkModeChange()
})
@@ -163,7 +170,16 @@ function handleDarkModeChange() {
function restartForm() {
previewFormSubmitted.value = false
formPreview.value.restart()
try {
// Try using the component reference first
if (formPreview.value && typeof formPreview.value.restart === 'function') {
formPreview.value.restart()
return
}
} catch (error) {
console.error('Error restarting form:', error)
}
}
function toggleExpand() {

View File

@@ -35,7 +35,7 @@
:options="visibilityOptions"
/>
<div
v-if="form.closes_at || form.visibility == 'closed'"
v-if="isFormClosingOrClosed"
class="bg-gray-50 border rounded-lg px-4 py-2"
>
<rich-text-area-input
@@ -112,119 +112,103 @@
</modal>
</template>
<script>
import clonedeep from "clone-deep"
import { default as _has } from "lodash/has"
<script setup>
import clonedeep from 'clone-deep'
import { default as _has } from 'lodash/has'
export default {
setup() {
const formsStore = useFormsStore()
const workingFormStore = useWorkingFormStore()
const { getAll: forms } = storeToRefs(formsStore)
// Store setup
const formsStore = useFormsStore()
const workingFormStore = useWorkingFormStore()
const { content: form } = storeToRefs(workingFormStore)
const forms = computed(() => formsStore.getAll)
// Reactive state
const showCopyFormSettingsModal = ref(false)
const copyFormId = ref(null)
// Computed properties
const visibilityOptions = [
{
name: 'Published',
value: 'public',
},
{
name: 'Draft - not publicly accessible',
value: 'draft',
},
{
name: 'Closed - won\'t accept new submissions',
value: 'closed',
},
]
const copyFormOptions = computed(() => {
return forms.value
.filter((formItem) => {
return form.value.id !== formItem.id
})
.map((formItem) => {
return {
name: formItem.title,
value: formItem.id,
}
})
})
const allTagsOptions = computed(() => {
return formsStore.allTags.map((tagname) => {
return {
forms,
formsStore,
workingFormStore,
name: tagname,
value: tagname,
}
},
})
})
data() {
return {
showCopyFormSettingsModal: false,
copyFormId: null,
visibilityOptions: [
{
name: "Published",
value: "public",
},
{
name: "Draft - not publicly accessible",
value: "draft",
},
{
name: "Closed - won't accept new submissions",
value: "closed",
},
],
}
},
// New computed property for v-if condition
const isFormClosingOrClosed = computed(() => {
return form.value.closes_at || form.value.visibility === 'closed'
})
computed: {
copyFormOptions() {
return this.forms
.filter((form) => {
return this.form.id !== form.id
})
.map((form) => {
return {
name: form.title,
value: form.id,
}
})
},
form: {
get() {
return this.workingFormStore.content
},
/* We add a setter */
set(value) {
this.workingFormStore.set(value)
},
},
allTagsOptions() {
return this.formsStore.allTags.map((tagname) => {
return {
name: tagname,
value: tagname,
}
})
},
},
// Methods
const copySettings = () => {
if (copyFormId.value == null)
return
const copyForm = clonedeep(
forms.value.find(form => form.id === copyFormId.value),
)
if (!copyForm)
return;
watch: {},
// Clean copy from form
[
"title",
"properties",
"cleanings",
"views_count",
"submissions_count",
"workspace",
"workspace_id",
"updated_at",
"share_url",
"slug",
"notion_database_url",
"id",
"database_id",
"database_fields_update",
"creator",
"created_at",
"deleted_at",
"last_edited_human",
].forEach((property) => {
if (_has(copyForm, property))
delete copyForm[property]
})
mounted() {},
methods: {
copySettings() {
if (this.copyFormId == null) return
const copyForm = clonedeep(
this.forms.find((form) => form.id === this.copyFormId),
)
if (!copyForm) return;
// Clean copy from form
[
"title",
"properties",
"cleanings",
"views_count",
"submissions_count",
"workspace",
"workspace_id",
"updated_at",
"share_url",
"slug",
"notion_database_url",
"id",
"database_id",
"database_fields_update",
"creator",
"created_at",
"deleted_at",
"last_edited_human",
].forEach((property) => {
if (_has(copyForm, property)) {
delete copyForm[property]
}
})
// Apply changes
Object.keys(copyForm).forEach((property) => {
this.form[property] = copyForm[property]
})
this.showCopyFormSettingsModal = false
},
},
// Apply changes
Object.keys(copyForm).forEach((property) => {
form.value[property] = copyForm[property]
})
showCopyFormSettingsModal.value = false
useAlert().success('Form settings copied.')
}
</script>

View File

@@ -50,6 +50,18 @@
Technical Docs
</a>
<template v-if="!useFeatureFlag('self_hosted')">
<router-link
:to="{ name: 'integrations' }"
class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue"
>
Integrations
</router-link>
<router-link
:to="{ name: 'report-abuse' }"
class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue"
>
Report Abuse
</router-link>
<router-link
:to="{ name: 'privacy-policy' }"
class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue"
@@ -63,13 +75,6 @@
>
Terms & Conditions
</router-link>
<router-link
:to="{ name: 'report-abuse' }"
class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue"
>
Report Abuse
</router-link>
</template>
</div>
</div>

View File

@@ -0,0 +1,68 @@
<template>
<div
v-if="innerJson"
id="custom-block"
>
<div
v-if="innerJson.type=='faq'"
class="rounded-lg bg-white z-10 pt-10"
>
<h2 class="font-medium">
Frequently Asked Questions
</h2>
<dl class="pt-4 space-y-6">
<div
v-for="question in innerJson.content"
:key="question.label"
class="space-y-2"
>
<dt class="font-semibold text-gray-900 dark:text-gray-100">
{{ question.label }}
</dt>
<dd
class="leading-6 text-gray-600 dark:text-gray-400"
v-html="question.content"
/>
</div>
</dl>
</div>
<div
v-else-if="innerJson.type=='cta'"
class="rounded-lg relative bg-gradient-to-r from-blue-400 to-blue-600 shadow-ld p-8 z-10"
>
<div class="absolute inset-px rounded-[calc(var(--radius)-1px)]">
<div class="flex justify-center w-full h-full">
<SpotlightCard
class="w-full p-2 rounded-[--radius] [--radius:theme(borderRadius.lg)] opacity-70"
from="#60a5fa"
:size="200"
/>
</div>
</div>
<div class="relative z-20 flex flex-col items-center gap-4 pb-1">
<h2 class="text-xl md:text-2xl text-center font-medium text-white">
{{ innerJson.title ? innerJson.title : 'Ready to upgrade your OpnForm forms?' }}
</h2>
<UButton
to="/register"
color="white"
class="hover:no-underline"
icon="i-heroicons-arrow-right"
trailing
>
Try OpnForm for free
</UButton>
</div>
</div>
</div>
</template>
<script setup>
import { blockProps } from 'vue-notion'
import useNotionBlock from '~/components/pages/notion/useNotionBlock.js'
const props = defineProps(blockProps)
const block = useNotionBlock(props)
const innerJson = computed(() => block.innerJson.value)
</script>

View File

@@ -0,0 +1,107 @@
import { computed } from 'vue'
export default function useNotionBlock (props) {
const block = computed(() => {
const id = props.contentId || Object.keys(props.blockMap)[0]
return props.blockMap[id]
})
const value = computed(() => {
return block.value?.value
})
const format = computed(() => {
return value.value?.format
})
const icon = computed(() => {
return format.value?.page_icon || ''
})
const width = computed(() => {
return format.value?.block_width
})
const properties = computed(() => {
return value.value?.properties
})
const caption = computed(() => {
return properties.value?.caption
})
const description = computed(() => {
return properties.value?.description
})
const src = computed(() => {
return mapImageUrl(properties.value?.source[0][0], block.value)
})
const title = computed(() => {
return properties.value?.title
})
const alt = computed(() => {
return caption.value?.[0][0]
})
const type = computed(() => {
return value.value?.type
})
const visible = computed(() => {
return !props.hideList.includes(type.value)
})
const hasPageLinkOptions = computed(() => {
return props.pageLinkOptions?.component && props.pageLinkOptions?.href
})
const parent = computed(() => {
return props.blockMap[value.value?.parent_id]
})
const innerJson = computed(() => {
if (type.value !== 'code') return
if (properties.value.language.flat('Infinity').join('') !== 'JSON') {
return
}
try {
return JSON.parse(
title.value.flat(Infinity).join('').replace(/\n/g, '').replace(/\t/g, '').trim()
)
} catch (error) {
console.error('Failed to parse JSON',
error,
title.value.flat(Infinity).join('').replace(/\n/g, '').replace(/\t/g, '').trim()
)
return
}
})
function mapImageUrl (source) {
// Implement your mapImageUrl logic here
return source
}
return {
icon,
width,
properties,
caption,
description,
src,
title,
alt,
block,
value,
format,
type,
visible,
hasPageLinkOptions,
parent,
innerJson
}
}

View File

@@ -54,7 +54,7 @@ const setupForUser = () => {
}
onMounted(() => {
if (import.meta.server) return
if (import.meta.server || !user.value) return
// Setup base
if (
@@ -66,7 +66,6 @@ onMounted(() => {
}
}
if (!user.value) return
loadScript()
try {
setupForUser()