inicontent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/.github/pull_request.yml +18 -0
  2. package/CONTRIBUTING.md +67 -0
  3. package/LICENSE +21 -0
  4. package/README.md +142 -0
  5. package/app/app.vue +106 -0
  6. package/app/assets/main.css +974 -0
  7. package/app/components/Api/EndpointCard.vue +169 -0
  8. package/app/components/Api/SchemaDocsTable.vue +208 -0
  9. package/app/components/Asset/Card.vue +1363 -0
  10. package/app/components/Asset/Grid.vue +295 -0
  11. package/app/components/Asset/Icon.vue +38 -0
  12. package/app/components/Asset/Preview.vue +486 -0
  13. package/app/components/Asset/Scanner.vue +700 -0
  14. package/app/components/Asset/Thumb.vue +708 -0
  15. package/app/components/Auth/Reset.vue +99 -0
  16. package/app/components/Auth/index.vue +346 -0
  17. package/app/components/ChatInterface.vue +2326 -0
  18. package/app/components/ChatPagePreview.vue +34 -0
  19. package/app/components/Column/Array.vue +29 -0
  20. package/app/components/Column/Asset.vue +21 -0
  21. package/app/components/Column/Boolean.vue +12 -0
  22. package/app/components/Column/Color.vue +9 -0
  23. package/app/components/Column/Date.vue +57 -0
  24. package/app/components/Column/Edit.vue +215 -0
  25. package/app/components/Column/Email.vue +9 -0
  26. package/app/components/Column/Html.vue +27 -0
  27. package/app/components/Column/Id.vue +20 -0
  28. package/app/components/Column/Locale.vue +32 -0
  29. package/app/components/Column/Object.vue +18 -0
  30. package/app/components/Column/Password.vue +7 -0
  31. package/app/components/Column/Role.vue +15 -0
  32. package/app/components/Column/S.vue +14 -0
  33. package/app/components/Column/Table/Single.vue +49 -0
  34. package/app/components/Column/Table/index.vue +42 -0
  35. package/app/components/Column/Tags.vue +31 -0
  36. package/app/components/Column/Text.vue +9 -0
  37. package/app/components/Column/Url.vue +9 -0
  38. package/app/components/Column/index.vue +41 -0
  39. package/app/components/Dashboard/Editor.vue +221 -0
  40. package/app/components/Dashboard/Grid.vue +270 -0
  41. package/app/components/Dashboard/View.vue +131 -0
  42. package/app/components/Dashboard/Widget/BarChart.vue +73 -0
  43. package/app/components/Dashboard/Widget/Counter.vue +51 -0
  44. package/app/components/Dashboard/Widget/LineChart.vue +76 -0
  45. package/app/components/Dashboard/Widget/PieChart.vue +75 -0
  46. package/app/components/Dashboard/Widget/Table.vue +94 -0
  47. package/app/components/Dashboard/WidgetEditor.vue +274 -0
  48. package/app/components/Data/Array.vue +102 -0
  49. package/app/components/Data/Html.vue +9 -0
  50. package/app/components/Data/Icon.vue +9 -0
  51. package/app/components/Data/Object.vue +10 -0
  52. package/app/components/Data/S.vue +16 -0
  53. package/app/components/Data/Table.vue +53 -0
  54. package/app/components/Data/Value.vue +34 -0
  55. package/app/components/Data/index.vue +14 -0
  56. package/app/components/Field/Array.vue +324 -0
  57. package/app/components/Field/Asset/Actions.vue +97 -0
  58. package/app/components/Field/Asset/index.vue +639 -0
  59. package/app/components/Field/Boolean.vue +26 -0
  60. package/app/components/Field/Checkbox.vue +48 -0
  61. package/app/components/Field/Color.vue +27 -0
  62. package/app/components/Field/Date.vue +30 -0
  63. package/app/components/Field/Email.vue +51 -0
  64. package/app/components/Field/EmailTemplate.vue +420 -0
  65. package/app/components/Field/Html/ColorPicker.vue +127 -0
  66. package/app/components/Field/Html/Editor.vue +544 -0
  67. package/app/components/Field/Html/index.vue +25 -0
  68. package/app/components/Field/Icon.vue +114 -0
  69. package/app/components/Field/JSON.vue +113 -0
  70. package/app/components/Field/Mention.vue +36 -0
  71. package/app/components/Field/Number.vue +27 -0
  72. package/app/components/Field/Object.vue +92 -0
  73. package/app/components/Field/Password.vue +55 -0
  74. package/app/components/Field/Radio.vue +47 -0
  75. package/app/components/Field/S.vue +44 -0
  76. package/app/components/Field/Select.vue +49 -0
  77. package/app/components/Field/Table.vue +348 -0
  78. package/app/components/Field/Tags.vue +113 -0
  79. package/app/components/Field/Text.vue +31 -0
  80. package/app/components/Field/Textarea.vue +33 -0
  81. package/app/components/Field/Url.vue +40 -0
  82. package/app/components/Field/Wrapper.vue +128 -0
  83. package/app/components/Field/index.vue +68 -0
  84. package/app/components/FloatingChatbot.vue +73 -0
  85. package/app/components/Form/Card.vue +151 -0
  86. package/app/components/Form/Drawer.vue +195 -0
  87. package/app/components/Form/index.vue +621 -0
  88. package/app/components/Header.vue +328 -0
  89. package/app/components/Offline/ConflictModal.vue +200 -0
  90. package/app/components/Offline/SyncStatus.vue +249 -0
  91. package/app/components/PlatformFeatureLanding.vue +65 -0
  92. package/app/components/Table/Backups.vue +638 -0
  93. package/app/components/Table/Flows.vue +814 -0
  94. package/app/components/Table/Grid.vue +296 -0
  95. package/app/components/Table/Icon.vue +36 -0
  96. package/app/components/Table/Logs.vue +181 -0
  97. package/app/components/Table/Schedules.vue +722 -0
  98. package/app/components/Table/Search/Button.vue +256 -0
  99. package/app/components/Table/Search/Items.vue +286 -0
  100. package/app/components/Table/Search/index.vue +87 -0
  101. package/app/components/Table/Settings/Schema.vue +801 -0
  102. package/app/components/Table/Settings/index.vue +631 -0
  103. package/app/components/Table/TranslateDrawer.vue +526 -0
  104. package/app/components/Table/Views/Kanban.vue +289 -0
  105. package/app/components/Table/Views/Table.vue +1273 -0
  106. package/app/components/Table/index.vue +890 -0
  107. package/app/composables/Translation/define.ts +41 -0
  108. package/app/composables/Translation/fetch.ts +107 -0
  109. package/app/composables/Translation/languages.ts +1 -0
  110. package/app/composables/Translation/loadCoreTranslations.ts +38 -0
  111. package/app/composables/Translation/translate.ts +250 -0
  112. package/app/composables/databaseCookies.ts +79 -0
  113. package/app/composables/fieldValidator.ts +72 -0
  114. package/app/composables/fieldsList.ts +301 -0
  115. package/app/composables/formatDatabase.ts +101 -0
  116. package/app/composables/index.ts +444 -0
  117. package/app/composables/openDrawer.ts +118 -0
  118. package/app/composables/optimizeFile.ts +287 -0
  119. package/app/composables/renderLabel.ts +88 -0
  120. package/app/composables/schemaClipboard.ts +95 -0
  121. package/app/composables/search.ts +353 -0
  122. package/app/composables/setThemeConfig.ts +48 -0
  123. package/app/composables/translationValue.ts +169 -0
  124. package/app/composables/useAssetPreview.ts +84 -0
  125. package/app/composables/useAssetUploader.ts +56 -0
  126. package/app/composables/useDashboardData.ts +310 -0
  127. package/app/composables/useDocumentThumbnail.ts +351 -0
  128. package/app/composables/useEcharts.ts +33 -0
  129. package/app/composables/useLocalDatabaseConfig.ts +51 -0
  130. package/app/composables/useOcr.ts +300 -0
  131. package/app/composables/useOfflineFetch.ts +194 -0
  132. package/app/composables/useOfflineItem.ts +107 -0
  133. package/app/composables/useOfflineQueue.ts +302 -0
  134. package/app/composables/useOfflineSync.ts +235 -0
  135. package/app/composables/usePasskeyAuth.ts +156 -0
  136. package/app/composables/usePdfCompressor.ts +180 -0
  137. package/app/composables/usePlatformRedirect.ts +72 -0
  138. package/app/composables/useRealtimeSync.ts +233 -0
  139. package/app/composables/useReferencedItems.ts +180 -0
  140. package/app/composables/useScanner.ts +664 -0
  141. package/app/composables/useSubscription.ts +650 -0
  142. package/app/composables/useThumbnailCache.ts +136 -0
  143. package/app/composables/useVideoCompressor.ts +194 -0
  144. package/app/error.vue +112 -0
  145. package/app/layouts/api.vue +188 -0
  146. package/app/layouts/dashboard.vue +91 -0
  147. package/app/layouts/default.vue +228 -0
  148. package/app/layouts/table.vue +471 -0
  149. package/app/locales/ar.ts +996 -0
  150. package/app/locales/en.ts +655 -0
  151. package/app/locales/es.ts +996 -0
  152. package/app/locales/fr.ts +997 -0
  153. package/app/middleware/dashboard.ts +33 -0
  154. package/app/middleware/database.ts +83 -0
  155. package/app/middleware/global.ts +1 -0
  156. package/app/middleware/sid-ingest.global.ts +29 -0
  157. package/app/middleware/table.ts +54 -0
  158. package/app/middleware/user.ts +26 -0
  159. package/app/pages/[[database]]/admin/api/auth/index.vue +306 -0
  160. package/app/pages/[[database]]/admin/api/index.vue +18 -0
  161. package/app/pages/[[database]]/admin/api/tables/[table]/index.vue +497 -0
  162. package/app/pages/[[database]]/admin/api/tables/index.vue +87 -0
  163. package/app/pages/[[database]]/admin/billing/index.vue +16 -0
  164. package/app/pages/[[database]]/admin/dashboards/[id]/edit.vue +66 -0
  165. package/app/pages/[[database]]/admin/dashboards/[id]/index.vue +117 -0
  166. package/app/pages/[[database]]/admin/dashboards/index.vue +20 -0
  167. package/app/pages/[[database]]/admin/index.vue +28 -0
  168. package/app/pages/[[database]]/admin/settings.vue +526 -0
  169. package/app/pages/[[database]]/admin/tables/[table]/[id]/edit.vue +143 -0
  170. package/app/pages/[[database]]/admin/tables/[table]/[id]/index.vue +146 -0
  171. package/app/pages/[[database]]/admin/tables/[table]/flows.vue +10 -0
  172. package/app/pages/[[database]]/admin/tables/[table]/index.vue +10 -0
  173. package/app/pages/[[database]]/admin/tables/[table]/new.vue +12 -0
  174. package/app/pages/[[database]]/admin/tables/[table]/schedules.vue +10 -0
  175. package/app/pages/[[database]]/admin/tables/[table]/settings.vue +30 -0
  176. package/app/pages/[[database]]/admin/tables/assets/[...path].vue +24 -0
  177. package/app/pages/[[database]]/admin/tables/assets/flows.vue +10 -0
  178. package/app/pages/[[database]]/admin/tables/backups/index.vue +10 -0
  179. package/app/pages/[[database]]/admin/tables/blocks/index.vue +16 -0
  180. package/app/pages/[[database]]/admin/tables/index.vue +20 -0
  181. package/app/pages/[[database]]/admin/tables/pages/index.vue +16 -0
  182. package/app/pages/[[database]]/admin/tables/templates/index.vue +16 -0
  183. package/app/pages/[[database]]/api/index.vue +120 -0
  184. package/app/pages/[[database]]/auth/index.vue +12 -0
  185. package/app/pages/[[database]]/auth/reset.vue +10 -0
  186. package/app/pages/[[database]]/index.vue +10 -0
  187. package/app/plugins/offline-warmup.client.ts +53 -0
  188. package/app/plugins/template-globals.ts +9 -0
  189. package/app/spa-loading-template.html +93 -0
  190. package/biome.json +53 -0
  191. package/index.d.ts +380 -0
  192. package/modules/naiveui.ts +41 -0
  193. package/nuxt.config.ts +269 -0
  194. package/package.json +56 -0
  195. package/pnpm-workspace.yaml +5 -0
  196. package/public/apple-touch-icon.png +0 -0
  197. package/public/favicon-16x16.png +0 -0
  198. package/public/favicon-32x32.png +0 -0
  199. package/public/favicon.ico +0 -0
  200. package/public/pwa-192x192.png +0 -0
  201. package/public/pwa-512x512.png +0 -0
  202. package/public/pwa-maskable-192x192.png +0 -0
  203. package/public/pwa-maskable-512x512.png +0 -0
  204. package/public/vendor/opencv.js +48 -0
  205. package/tsconfig.json +9 -0
@@ -0,0 +1,48 @@
1
+ <template>
2
+ <FieldWrapper :field :rule v-model="modelValue">
3
+ <NCheckboxGroup v-model:value="modelValue" v-bind="field.inputProps
4
+ ? typeof field.inputProps === 'function'
5
+ ? field.inputProps(modelValue) ?? {}
6
+ : field.inputProps
7
+ : {}">
8
+ <NFlex>
9
+ <NCheckbox v-for="{ label, value } in options" :value :label />
10
+ </NFlex>
11
+ </NCheckboxGroup>
12
+ </FieldWrapper>
13
+ </template>
14
+
15
+ <script lang="ts" setup>
16
+ import { isArrayOfArrays, isArrayOfObjects } from "inibase/utils"
17
+ import type { FormItemRule } from "naive-ui"
18
+
19
+ const { field } = defineProps<{ field: Field }>()
20
+
21
+ const modelValue = defineModel<string | number | (string | number)[]>()
22
+
23
+ const rule: FormItemRule = {
24
+ trigger: "change",
25
+ type: "array",
26
+ required: field.required,
27
+ validator: async () => {
28
+ await nextTick()
29
+ return fieldValidator(field, modelValue.value)
30
+ },
31
+ }
32
+
33
+ const options = computed(() =>
34
+ field.options
35
+ ? isArrayOfArrays(field.options)
36
+ ? (field.options as [string | number, string][]).map(([value]) => ({
37
+ value: value,
38
+ label: t(value),
39
+ }))
40
+ : isArrayOfObjects(field.options)
41
+ ? field.options
42
+ : (field.options as string[]).map((value) => ({
43
+ value: value,
44
+ label: t(value),
45
+ }))
46
+ : [],
47
+ )
48
+ </script>
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <FieldWrapper :field :rule v-model="modelValue">
3
+ <NColorPicker :modes="['hex']" :actions="['clear']" v-model:value="modelValue" v-bind="field.inputProps
4
+ ? typeof field.inputProps === 'function'
5
+ ? field.inputProps(modelValue) ?? {}
6
+ : field.inputProps
7
+ : {}" />
8
+ </FieldWrapper>
9
+ </template>
10
+
11
+ <script lang="ts" setup>
12
+ import type { FormItemRule } from "naive-ui"
13
+
14
+ const { field } = defineProps<{ field: Field }>()
15
+
16
+ const modelValue = defineModel<string>()
17
+
18
+ const rule: FormItemRule = {
19
+ trigger: ["blur", "change"],
20
+ type: "hex",
21
+ required: field.required,
22
+ validator: async () => {
23
+ await nextTick()
24
+ return fieldValidator(field, modelValue.value)
25
+ },
26
+ }
27
+ </script>
@@ -0,0 +1,30 @@
1
+ <template>
2
+ <FieldWrapper :field :rule v-model="modelValue">
3
+ <NDatePicker :month-format="field.date && field.date === 'month' ? 'MMM' : 'MM'"
4
+ :format="field.date && ['datetime', 'datetimerange'].includes(field.date) ? 'dd-MM-yyyy HH:mm:ss' : 'dd-MM-yyyy'"
5
+ :type="field.date ?? 'date'" :actions="field.date && ['datetime', 'datetimerange'].includes(field.date) ? ['confirm'] : null" v-model:value="modelValue" clearable
6
+ :placeholder="t(field.key)" v-bind="field.inputProps
7
+ ? typeof field.inputProps === 'function'
8
+ ? field.inputProps(modelValue) ?? {}
9
+ : field.inputProps
10
+ : {}" />
11
+ </FieldWrapper>
12
+ </template>
13
+
14
+ <script lang="ts" setup>
15
+ import type { FormItemRule } from "naive-ui"
16
+
17
+ const { field } = defineProps<{ field: Field }>()
18
+
19
+ const modelValue = defineModel<number>()
20
+
21
+ const rule: FormItemRule = {
22
+ trigger: "change",
23
+ type: "number",
24
+ required: field.required,
25
+ validator: async () => {
26
+ await nextTick()
27
+ return fieldValidator(field, modelValue.value)
28
+ },
29
+ }
30
+ </script>
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <FieldWrapper :field :rule v-model="modelValue">
3
+ <NAutoComplete :options v-model:value="modelValue" :placeholder="t(field.key)" clearable v-bind="field.inputProps
4
+ ? typeof field.inputProps === 'function'
5
+ ? { ...(field.inputProps(modelValue) ?? {}), type: 'email', autocomplete: 'disabled' }
6
+ : { ...field.inputProps, type: 'email', autocomplete: 'disabled' }
7
+ : { type: 'email', autocomplete: 'disabled' }">
8
+ <template #suffix>
9
+ <component :is="getField(field).icon" />
10
+ </template>
11
+ </NAutoComplete>
12
+ </FieldWrapper>
13
+ </template>
14
+
15
+ <script lang="ts" setup>
16
+ import { isEmail } from "inibase/utils"
17
+ import type { FormItemRule } from "naive-ui"
18
+
19
+ const { field } = defineProps<{ field: Field }>()
20
+
21
+ const modelValue = defineModel<string>()
22
+
23
+ const rule: FormItemRule = {
24
+ trigger: ["blur", "input"],
25
+ type: "email",
26
+ required: field.required,
27
+ validator: async () => {
28
+ await nextTick()
29
+ return fieldValidator(field, modelValue.value, isEmail)
30
+ },
31
+ }
32
+ const emailProviders = [
33
+ "@gmail.com",
34
+ "@outlook.com",
35
+ "@yahoo.com",
36
+ "@hotmail.com",
37
+ "@protonmail.com",
38
+ "@qq.com",
39
+ ]
40
+
41
+ const options = computed(() => {
42
+ return emailProviders.map((suffix) => {
43
+ const value = !modelValue.value ? "" : modelValue.value
44
+ const prefix = value?.split("@")[0]
45
+ return {
46
+ label: prefix + suffix,
47
+ value: prefix + suffix,
48
+ }
49
+ })
50
+ })
51
+ </script>
@@ -0,0 +1,420 @@
1
+ <template>
2
+ <FieldWrapper :field :rule v-model="modelValue">
3
+ <NFlex vertical :size="12" style="width: 100%">
4
+ <NFlex justify="space-between" align="center" :size="8" wrap>
5
+ <!-- Variable chips -->
6
+ <NFlex wrap align="center" :size="8">
7
+ <NText depth="3" style="font-size: 12px; white-space: nowrap;">
8
+ {{ t("emailTemplates.variables") }}
9
+ </NText>
10
+
11
+ <NTag
12
+ v-for="variable in allVariables"
13
+ :key="variable"
14
+ size="small"
15
+ round
16
+ :bordered="false"
17
+ :type="builtinVariables.includes(variable) ? 'info' : 'warning'"
18
+ :closable="!builtinVariables.includes(variable)"
19
+ style="cursor: pointer"
20
+ @click="insert(variable)"
21
+ @close="removeCustomVariable(variable)"
22
+ >
23
+ {{ variableTag(variable) }}
24
+ </NTag>
25
+
26
+ <NPopover
27
+ trigger="click"
28
+ placement="bottom"
29
+ :show="showAddVariable"
30
+ @update:show="(value: boolean) => (showAddVariable = value)"
31
+ >
32
+ <template #trigger>
33
+ <NButton size="tiny" quaternary type="primary" @click="showAddVariable = !showAddVariable">
34
+ <template #icon>
35
+ <NIcon>
36
+ <Icon name="tabler:plus" />
37
+ </NIcon>
38
+ </template>
39
+ </NButton>
40
+ </template>
41
+ <NFlex vertical :size="8" style="width: 260px">
42
+ <NInput
43
+ v-model:value="newVariable"
44
+ size="small"
45
+ :placeholder="t('emailTemplates.variableName')"
46
+ @keydown.enter.prevent="addCustomVariable"
47
+ />
48
+ <NButton
49
+ size="small"
50
+ type="primary"
51
+ :disabled="!newVariable.trim()"
52
+ @click="addCustomVariable"
53
+ >
54
+ {{ t("emailTemplates.add") }}
55
+ </NButton>
56
+ </NFlex>
57
+ </NPopover>
58
+ </NFlex>
59
+
60
+ <!-- Send a test email with the current (possibly unsaved) template -->
61
+ <NPopover
62
+ trigger="click"
63
+ placement="bottom-end"
64
+ :show="showTestPopover"
65
+ @update:show="(value: boolean) => (showTestPopover = value)"
66
+ >
67
+ <template #trigger>
68
+ <NButton
69
+ size="tiny"
70
+ secondary
71
+ round
72
+ :loading="Loading.testEmail"
73
+ @click="showTestPopover = !showTestPopover"
74
+ >
75
+ <template #icon>
76
+ <NIcon>
77
+ <Icon name="tabler:send" />
78
+ </NIcon>
79
+ </template>
80
+ {{ t("emailTemplates.sendTest") }}
81
+ </NButton>
82
+ </template>
83
+ <NFlex vertical :size="8" style="width: 300px">
84
+ <NText depth="3" style="font-size: 12px;">
85
+ {{ t("emailTemplates.sendTestHint") }}
86
+ </NText>
87
+ <NInput
88
+ v-model:value="testRecipient"
89
+ size="small"
90
+ :placeholder="t('emailTemplates.testRecipient')"
91
+ />
92
+ <NButton
93
+ size="small"
94
+ type="primary"
95
+ :loading="Loading.testEmail"
96
+ :disabled="!isEmail(testRecipient)"
97
+ @click="sendTest"
98
+ >
99
+ {{ t("sendTestEmail") }}
100
+ </NButton>
101
+ </NFlex>
102
+ </NPopover>
103
+ </NFlex>
104
+
105
+ <NText depth="3" style="font-size: 12px;">
106
+ {{ t("emailTemplates.recordFieldsHint") }}
107
+ </NText>
108
+
109
+ <!-- MJML content editor -->
110
+ <NInput
111
+ ref="inputRef"
112
+ v-model:value="modelValue"
113
+ type="textarea"
114
+ :placeholder="t('emailTemplates.contentPlaceholder')"
115
+ :autosize="{ minRows: 12, maxRows: 36 }"
116
+ style="font-family: monospace"
117
+ />
118
+
119
+ <!-- Live preview — compiled server-side, rendered in a sandboxed iframe -->
120
+ <NCollapse>
121
+ <NCollapseItem :name="'preview'" :title="t('emailTemplates.preview')">
122
+ <NAlert
123
+ v-if="!previewHtml && !previewText && !previewLoading"
124
+ type="info"
125
+ :bordered="false"
126
+ >
127
+ {{ t("emailTemplates.previewEmpty") }}
128
+ </NAlert>
129
+ <NText v-if="previewLoading" depth="3" style="font-size: 12px;">
130
+ {{ t("emailTemplates.previewLoading") }}
131
+ </NText>
132
+ <template v-else>
133
+ <NText
134
+ v-if="previewSubject"
135
+ depth="3"
136
+ class="email-template-subject"
137
+ >
138
+ {{ t("emailTemplates.previewSubject") }}: {{ previewSubject }}
139
+ </NText>
140
+ <!-- sandbox="" blocks scripts inside the preview so template
141
+ markup can never affect the CMS or the editor session. -->
142
+ <iframe
143
+ v-if="previewHtml"
144
+ class="email-template-preview-frame"
145
+ :srcdoc="previewHtml"
146
+ sandbox=""
147
+ ></iframe>
148
+ <pre v-else-if="previewText" class="email-template-preview">{{
149
+ previewText
150
+ }}</pre>
151
+ </template>
152
+ </NCollapseItem>
153
+ </NCollapse>
154
+ </NFlex>
155
+ </FieldWrapper>
156
+ </template>
157
+
158
+ <script lang="ts" setup>
159
+ import type { FormItemRule } from "naive-ui";
160
+
161
+ const { field, item } = defineProps<{ field: Field; item?: Item }>();
162
+
163
+ const modelValue = defineModel<string>();
164
+
165
+ const rule: FormItemRule = {
166
+ trigger: ["blur", "input"],
167
+ required: field.required,
168
+ validator: async () => {
169
+ await nextTick();
170
+ return fieldValidator(field, modelValue.value);
171
+ },
172
+ };
173
+
174
+ import { isEmail } from "inibase/utils";
175
+
176
+ // Placeholders that flows always provide when sending through the `email`
177
+ // rule (see api/utils/handleFlow.ts):
178
+ // @user.* — current user fields
179
+ // @data.* — fields of the record that triggered the email (also available
180
+ // bare, e.g. {{status}})
181
+ const builtinVariables = [
182
+ "@user.id",
183
+ "@user.username",
184
+ "@user.email",
185
+ "@user.role",
186
+ ];
187
+
188
+ const CUSTOM_VARIABLES_KEY = "emailTemplates.customVariables";
189
+
190
+ // Sample values used only for the preview and test emails — never sent.
191
+ const sampleValues: Record<string, string> = {
192
+ "@user.id": "e7b4c2f8a1d9",
193
+ "@user.username": "karim",
194
+ "@user.email": "karim@example.com",
195
+ "@user.role": "1",
196
+ };
197
+
198
+ // Placeholder syntax: {{name}}, {{@data.field}}, {{@user.field.name}}, …
199
+ const PLACEHOLDER_PATTERN = /\{\{([@\w][\w.-]*)\}\}/g;
200
+
201
+ const customVariables = ref<string[]>([]);
202
+ function loadCustomVariables() {
203
+ if (!import.meta.client) return;
204
+ try {
205
+ const raw = localStorage.getItem(CUSTOM_VARIABLES_KEY);
206
+ if (raw) customVariables.value = JSON.parse(raw);
207
+ } catch {
208
+ customVariables.value = [];
209
+ }
210
+ }
211
+ loadCustomVariables();
212
+
213
+ const allVariables = computed(() => [
214
+ ...new Set([...builtinVariables, ...customVariables.value]),
215
+ ]);
216
+
217
+ // Rendered in the template as `{{variable}}` — kept as a function because the
218
+ // literal `}}` inside an interpolation would close the Vue expression early.
219
+ const variableTag = (key: string) => `{{${key}}}`;
220
+
221
+ const showAddVariable = ref(false);
222
+ const newVariable = ref("");
223
+
224
+ function persistCustomVariables() {
225
+ if (!import.meta.client) return;
226
+ try {
227
+ localStorage.setItem(
228
+ CUSTOM_VARIABLES_KEY,
229
+ JSON.stringify(customVariables.value),
230
+ );
231
+ } catch {
232
+ // localStorage unavailable — keep in-memory list
233
+ }
234
+ }
235
+
236
+ function addCustomVariable() {
237
+ const key = newVariable.value.trim().replace(/[{}]/g, "");
238
+ if (!key) return;
239
+ if (!customVariables.value.includes(key)) {
240
+ customVariables.value.push(key);
241
+ persistCustomVariables();
242
+ }
243
+ newVariable.value = "";
244
+ showAddVariable.value = false;
245
+ insert(key);
246
+ }
247
+
248
+ function removeCustomVariable(key: string) {
249
+ customVariables.value = customVariables.value.filter(
250
+ (variable) => variable !== key,
251
+ );
252
+ persistCustomVariables();
253
+ }
254
+
255
+ const inputRef = ref();
256
+
257
+ // Insert {{token}} at the textarea cursor position, falling back to append.
258
+ function insert(key: string) {
259
+ const textarea = inputRef.value?.textareaElRef?.value as
260
+ | HTMLTextAreaElement
261
+ | undefined;
262
+ const token = `{{${key}}}`;
263
+ const current = modelValue.value ?? "";
264
+
265
+ if (!textarea) {
266
+ modelValue.value = current + token;
267
+ return;
268
+ }
269
+
270
+ const start = textarea.selectionStart;
271
+ const end = textarea.selectionEnd;
272
+ modelValue.value = current.slice(0, start) + token + current.slice(end);
273
+
274
+ nextTick(() => {
275
+ textarea.focus();
276
+ const position = start + token.length;
277
+ textarea.setSelectionRange(position, position);
278
+ });
279
+ }
280
+
281
+ // ── Live preview (compiled server-side, rendered in a sandboxed iframe) ────────
282
+ const database = useState<Database>("database");
283
+ const sessionID = useScopedCookie<string>("sid", database.value?.slug);
284
+ const config = useRuntimeConfig();
285
+ const Loading = useState<Record<string, boolean>>("Loading", () => ({}));
286
+
287
+ const previewHtml = ref("");
288
+ const previewText = ref("");
289
+ const previewSubject = ref("");
290
+ const previewLoading = ref(false);
291
+ let previewTimer: ReturnType<typeof setTimeout> | undefined;
292
+
293
+ // Build sample variables from every placeholder used in the template so the
294
+ // preview and test emails demonstrate real substitution.
295
+ function sampleVariables(): Record<string, string> {
296
+ const variables: Record<string, string> = { ...sampleValues };
297
+ const template = `${item?.subject ?? ""}\n${modelValue.value ?? ""}`;
298
+ for (const match of template.matchAll(PLACEHOLDER_PATTERN)) {
299
+ const key = match[1];
300
+ if (key && !(key in variables))
301
+ variables[key] = `sample_${key.replace(/^@/, "").replace(/\./g, "_")}`;
302
+ }
303
+ return variables;
304
+ }
305
+
306
+ // Compile the current (possibly unsaved) template on the server and render it
307
+ // in a sandboxed iframe. The sandbox disables scripts, so arbitrary images or
308
+ // HTML/MJML markup in a template cannot affect the CMS or the editor session.
309
+ // Debounced so typing in the editor does not hammer the server.
310
+ async function refreshPreview() {
311
+ const template = (modelValue.value ?? "").trim();
312
+ if (!template || !import.meta.client) {
313
+ previewHtml.value = "";
314
+ previewText.value = "";
315
+ previewSubject.value = "";
316
+ return;
317
+ }
318
+ previewLoading.value = true;
319
+ try {
320
+ const response = await $fetch<
321
+ apiResponse<{ html: string; subject: string; text: string }>
322
+ >(
323
+ `${config.public.apiBase}inicontent/databases/${database.value.slug}/email/preview`,
324
+ {
325
+ method: "POST",
326
+ credentials: "include",
327
+ query: { [`${database.value.slug}_sid`]: sessionID.value },
328
+ body: {
329
+ templateContent: modelValue.value ?? "",
330
+ subject: item?.subject ?? "",
331
+ variables: sampleVariables(),
332
+ },
333
+ },
334
+ );
335
+ previewHtml.value = response.result?.html ?? "";
336
+ previewText.value = response.result?.text ?? "";
337
+ previewSubject.value = response.result?.subject ?? "";
338
+ } catch {
339
+ // Preview is best-effort — never interrupt the editor.
340
+ previewHtml.value = "";
341
+ previewText.value = "";
342
+ previewSubject.value = "";
343
+ } finally {
344
+ previewLoading.value = false;
345
+ }
346
+ }
347
+
348
+ watch(
349
+ [modelValue, () => item?.subject],
350
+ () => {
351
+ clearTimeout(previewTimer);
352
+ previewTimer = setTimeout(refreshPreview, 400);
353
+ },
354
+ { immediate: true },
355
+ );
356
+
357
+ onBeforeUnmount(() => clearTimeout(previewTimer));
358
+
359
+ // ── Send a test email with the current (possibly unsaved) template ─────────────
360
+
361
+ const testRecipient = ref("");
362
+ const showTestPopover = ref(false);
363
+
364
+ async function sendTest() {
365
+ if (!isEmail(testRecipient.value)) return;
366
+
367
+ Loading.value.testEmail = true;
368
+ try {
369
+ const data = await $fetch<apiResponse>(
370
+ `${config.public.apiBase}inicontent/databases/${database.value.slug}/email/test`,
371
+ {
372
+ method: "POST",
373
+ credentials: "include",
374
+ query: { [`${database.value.slug}_sid`]: sessionID.value },
375
+ body: {
376
+ email: testRecipient.value,
377
+ templateContent: modelValue.value ?? "",
378
+ subject: item?.subject ?? "",
379
+ variables: sampleVariables(),
380
+ },
381
+ },
382
+ );
383
+ if (data.result) window.$message.success(data.message);
384
+ else window.$message.error(data.message);
385
+ } catch (error: unknown) {
386
+ const message = (error as { data?: { message?: string } })?.data?.message;
387
+ window.$message.error(message ?? t("emailSendFailed"));
388
+ } finally {
389
+ Loading.value.testEmail = false;
390
+ showTestPopover.value = false;
391
+ }
392
+ }
393
+ </script>
394
+
395
+ <style scoped>
396
+ .email-template-preview {
397
+ background-color: rgba(128, 128, 128, 0.08);
398
+ border-radius: 6px;
399
+ margin: 0;
400
+ padding: 12px;
401
+ white-space: pre-wrap;
402
+ word-break: break-word;
403
+ max-height: 320px;
404
+ overflow: auto;
405
+ }
406
+
407
+ .email-template-subject {
408
+ display: block;
409
+ font-size: 12px;
410
+ margin-bottom: 8px;
411
+ }
412
+
413
+ .email-template-preview-frame {
414
+ width: 100%;
415
+ height: 480px;
416
+ border: 1px solid rgba(128, 128, 128, 0.3);
417
+ border-radius: 6px;
418
+ background-color: #ffffff;
419
+ }
420
+ </style>
@@ -0,0 +1,127 @@
1
+ <template>
2
+ <div class="colorPicker">
3
+ <div v-for="colorsGroup in colors">
4
+ <span v-for="color in colorsGroup" :style="{ background: color }" @click="modelValue = color"></span>
5
+ </div>
6
+ </div>
7
+ </template>
8
+
9
+ <script lang="ts" setup>
10
+ const modelValue = defineModel<string>({ default: "" })
11
+
12
+ const colors = [
13
+ [
14
+ "#d3d9e3",
15
+ "#feccd1",
16
+ "#f8e5ad",
17
+ "#bbe3cd",
18
+ "#bcefe8",
19
+ "#b2e4fb",
20
+ "#aec0fd",
21
+ "#c4bfd9",
22
+ "#d5bccb",
23
+ "#d1c4c0",
24
+ ],
25
+ [
26
+ "#afb7cc",
27
+ "#fd9897",
28
+ "#f3d57a",
29
+ "#8fd1af",
30
+ "#91e6db",
31
+ "#81d3f9",
32
+ "#88a3f7",
33
+ "#9f96c0",
34
+ "#c391ab",
35
+ "#b39f99",
36
+ ],
37
+ [
38
+ "#939cb0",
39
+ "#f25c4e",
40
+ "#ecb81b",
41
+ "#30b479",
42
+ "#00d6c4",
43
+ "#2ab5f4",
44
+ "#5874cb",
45
+ "#7e6bab",
46
+ "#a8537b",
47
+ "#816257",
48
+ ],
49
+ [
50
+ "#71798b",
51
+ "#e84532",
52
+ "#e9a00f",
53
+ "#34945f",
54
+ "#0fb2a4",
55
+ "#0c9be3",
56
+ "#2449ac",
57
+ "#584a8e",
58
+ "#953963",
59
+ "#614238",
60
+ ],
61
+ [
62
+ "#5d6576",
63
+ "#d63b2c",
64
+ "#e98e0d",
65
+ "#227246",
66
+ "#008a7f",
67
+ "#0a88cf",
68
+ "#183fa0",
69
+ "#4f4082",
70
+ "#803459",
71
+ "#5c3f36",
72
+ ],
73
+ [
74
+ "#3e4656",
75
+ "#c93525",
76
+ "#e97d0a",
77
+ "#1b5a38",
78
+ "#016a5f",
79
+ "#0977bb",
80
+ "#0d3594",
81
+ "#473675",
82
+ "#6d2f50",
83
+ "#4d332d",
84
+ ],
85
+ [
86
+ "#1c2433",
87
+ "#ba2a19",
88
+ "#e95c06",
89
+ "#15482c",
90
+ "#014e42",
91
+ "#06579a",
92
+ "#042380",
93
+ "#39255e",
94
+ "#4b253f",
95
+ "#3d2622",
96
+ ],
97
+ ]
98
+ </script>
99
+
100
+ <style scoped>
101
+ .colorPicker {
102
+ display: flex;
103
+ flex-direction: column-reverse;
104
+ border-radius: 5px;
105
+ overflow: hidden;
106
+ border: 2px solid #fff;
107
+ }
108
+
109
+ .dark .colorPicker {
110
+ border-color: #000;
111
+ }
112
+
113
+ .colorPicker div {
114
+ display: flex;
115
+ }
116
+
117
+ .colorPicker div span {
118
+ width: 20px;
119
+ height: 20px;
120
+ }
121
+
122
+ .colorPicker div span:hover {
123
+ transform: scale(1.2);
124
+ box-shadow: 0 0 8px #00000011;
125
+ border-radius: 5px;
126
+ }
127
+ </style>