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,34 @@
1
+ <template>
2
+ <NModal
3
+ :show="show"
4
+ @update:show="emit('update:show', $event)"
5
+ preset="card"
6
+ :title="title"
7
+ size="large"
8
+ >
9
+ <NFlex v-if="page" vertical align="start" :size="8">
10
+ <NButton size="small" round secondary disabled>
11
+ <template #icon>
12
+ <NIcon><Icon name="tabler:file-text" /></NIcon>
13
+ </template>
14
+ /{{ page.slug }}
15
+ </NButton>
16
+ <NText depth="3">{{ t('pagePreviewUnavailable') }}</NText>
17
+ </NFlex>
18
+ </NModal>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ const props = defineProps<{
23
+ show: boolean;
24
+ page?: { slug?: string; seo?: { title?: string }; content?: unknown } | null;
25
+ primaryColor?: string;
26
+ }>();
27
+
28
+ const emit = defineEmits<{ "update:show": [boolean] }>();
29
+
30
+ const title = computed(() => {
31
+ const pageTitle = props.page?.seo?.title;
32
+ return pageTitle === "" ? t("homePage") : pageTitle || "";
33
+ });
34
+ </script>
@@ -0,0 +1,29 @@
1
+ <template>
2
+ <LazyColumn v-if="field.subType" :field="{ ...field, type: field.subType }" :value="value" />
3
+ <LazyColumn v-else-if="!isArrayOfObjects(field.children)"
4
+ :field="{ ...field, type: field.children === 'table' ? 'table' : 'tags' }" :value="value" />
5
+ <NPopover v-else scrollable style="max-height: 240px;" trigger="click">
6
+ <template #trigger>
7
+ <NButton size="small" circle>{{ `[${([] as Record<string, any>[]).concat(value).length}]` }}</NButton>
8
+ </template>
9
+ <NCollapse accordion arrow-placement="right">
10
+ <NCollapseItem v-for="(singleValue, index) in ([] as Record<string, any>[]).concat(value)"
11
+ :title="getCollapseItemTitle(field, (singleValue as Item), index)">
12
+ <NFlex vertical>
13
+ <NFlex
14
+ v-for="child in (field.children as Schema).filter(({ key }) => typeof singleValue[key] !== 'undefined')"
15
+ align="center" inline>
16
+ <strong>{{ child.key }}:</strong>
17
+ <LazyColumn :field="child" :value="singleValue[child.key]" />
18
+ </NFlex>
19
+ </NFlex>
20
+ </NCollapseItem>
21
+ </NCollapse>
22
+ </NPopover>
23
+ </template>
24
+
25
+ <script lang="ts" setup>
26
+ import { isArrayOfObjects } from "inibase/utils"
27
+
28
+ const { field, value } = defineProps<{ field: Field; value?: any }>()
29
+ </script>
@@ -0,0 +1,21 @@
1
+ <template>
2
+ <NFlex :wrap="false" class="list">
3
+ <template v-for="file in assets" :key="file.id">
4
+ <LazyAssetThumb :asset="file" :preview-assets="assets"
5
+ :container-selector="$route.params.id ? undefined : '#DataTable'" :size="40" />
6
+ </template>
7
+ </NFlex>
8
+ </template>
9
+
10
+ <script lang="ts" setup>
11
+ const { value } = defineProps<{ value: Asset | Asset[] }>();
12
+ const assets = computed(() => ([] as Asset[]).concat(value));
13
+ </script>
14
+
15
+ <style scoped>
16
+ .list {
17
+ max-width: 100%;
18
+ overflow: hidden;
19
+ min-width: 0;
20
+ }
21
+ </style>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <NIconWrapper :color="value === true ? undefined : '#D32F2F'" :border-radius="50" :size="18">
3
+ <NIcon size="16">
4
+ <Icon name="tabler:check" v-if="value === true" />
5
+ <Icon name="tabler:x" v-else />
6
+ </NIcon>
7
+ </NIconWrapper>
8
+ </template>
9
+
10
+ <script lang="ts" setup>
11
+ const { value } = defineProps<{ value?: boolean }>()
12
+ </script>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <NTag round :style="{ backgroundColor: value }">
3
+ <NText style="mix-blend-mode: difference;">{{ value }}</NText>
4
+ </NTag>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ const { value } = defineProps<{ value?: string }>()
9
+ </script>
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <NTooltip :delay="isRelativeDateNotifShown ? 2500 : 500" trigger="hover"
3
+ @update:show="(show) => !show ? isRelativeDateNotifShown = true : ''">
4
+ <template #trigger>
5
+ <NTime v-if="timeValue !== undefined" style="cursor: pointer;" @click.prevent="isRelative = !isRelative"
6
+ :time="timeValue" :type="isRelative ? 'relative' : absoluteType">
7
+ {{ value }}
8
+ </NTime>
9
+ <NText v-else :depth="3">{{ value }}</NText>
10
+ </template>
11
+ {{ t('clickToToggleDate') }}
12
+ </NTooltip>
13
+ </template>
14
+
15
+ <script lang="ts" setup>
16
+ const datetimeFieldTypes = ["datetime", "datetimerange"]
17
+
18
+ const isRelative = useState("isRelative", () => false)
19
+ const isRelativeDateNotifShown = useState(
20
+ "isRelativeDateNotifShown",
21
+ () => false,
22
+ )
23
+ const { value, field } = defineProps<{ value?: number | string | Date; field?: Field }>()
24
+
25
+ const timeValue = computed<number | undefined>(() => {
26
+ if (value === null || value === undefined) return undefined
27
+
28
+ if (value instanceof Date) {
29
+ const ms = value.getTime()
30
+ return Number.isNaN(ms) ? undefined : ms
31
+ }
32
+
33
+ if (typeof value === "number")
34
+ return Number.isNaN(value) ? undefined : value
35
+
36
+ if (typeof value === "string") {
37
+ const trimmed = value.trim()
38
+ if (!trimmed) return undefined
39
+
40
+ const numericValue = Number(trimmed)
41
+ if (!Number.isNaN(numericValue)) return numericValue
42
+
43
+ const parsedMs = Date.parse(trimmed)
44
+ return Number.isNaN(parsedMs) ? undefined : parsedMs
45
+ }
46
+
47
+ return undefined
48
+ })
49
+
50
+ const hasTime = computed(() => {
51
+ return !!field?.date && datetimeFieldTypes.includes(field.date)
52
+ })
53
+
54
+ const absoluteType = computed<"date" | "datetime">(() =>
55
+ hasTime.value ? "datetime" : "date",
56
+ )
57
+ </script>
@@ -0,0 +1,215 @@
1
+ <template>
2
+ <NTooltip :show="tooltipShow && !isEdit && isEditable" @update:show="(show) => tooltipShow = show" :delay="1500"
3
+ placement="bottom">
4
+ <template #trigger>
5
+ <NSpin
6
+ :show="!!loadingRef"
7
+ size="small"
8
+ style="min-height: 22px"
9
+ :onContextmenu="onContextmenu"
10
+ :onTouchstart="handleTouchStart"
11
+ :onTouchend="clearLongPress"
12
+ :onTouchcancel="clearLongPress"
13
+ :onTouchmove="clearLongPress"
14
+ :class="{ 'editable': !isEdit && isEditable }"
15
+ >
16
+ <LazyColumn v-if="!isEdit || isModalEdit" :field="fieldRef" :value="currentValue" :item-label="props.itemLabel" />
17
+ <Field v-else-if="isEdit" :field="inputField" v-model="inputValue" />
18
+ </NSpin>
19
+ </template>
20
+ {{ editHint }}
21
+ </NTooltip>
22
+
23
+ <NModal
24
+ v-if="isModalEdit"
25
+ :show="isEdit"
26
+ preset="card"
27
+ :style="modalStyle"
28
+ :title="modalTitle"
29
+ @update:show="(show) => (!show ? handleChange() : undefined)"
30
+ >
31
+ <Field :field="inputField" v-model="inputValue" />
32
+ <template #footer>
33
+ <NFlex justify="end">
34
+ <NButton secondary @click="cancelEdit">{{ t('cancel') }}</NButton>
35
+ <NButton type="primary" @click="handleChange">{{ t('save') }}</NButton>
36
+ </NFlex>
37
+ </template>
38
+ </NModal>
39
+ </template>
40
+
41
+ <script lang="ts" setup>
42
+ import { isArrayOfObjects } from "inibase/utils"
43
+
44
+ const props = defineProps<{
45
+ field: Field
46
+ loading?: boolean
47
+ modelValue?: unknown
48
+ editKey?: string
49
+ itemLabel?: string
50
+ }>()
51
+
52
+ const fieldRef = toRef(props, "field")
53
+ const loadingRef = toRef(props, "loading")
54
+ const editKeyRef = toRef(props, "editKey")
55
+
56
+ const emit = defineEmits(["update:modelValue"])
57
+
58
+ const currentValue = computed({
59
+ get: () => props.modelValue,
60
+ set: (newValue) => emit("update:modelValue", newValue),
61
+ })
62
+
63
+ // ── When the current language isn't the primary language, disable inline table edit feature ──
64
+ const isEditable = computed(
65
+ () => !isArrayOfObjects(fieldRef.value.children),
66
+ )
67
+ const isAssetField = computed(
68
+ () => fieldRef.value.type === "asset" || fieldRef.value.table === "assets",
69
+ )
70
+ const isModalEdit = computed(
71
+ () => fieldRef.value.type === "html" || isAssetField.value,
72
+ )
73
+
74
+ const tooltipShow = ref(false)
75
+ const isTouchDevice = ref(false)
76
+ const longPressTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
77
+ const editHint = computed(() =>
78
+ t(isTouchDevice.value ? "pressAndHoldToEdit" : "rightClickToEdit"),
79
+ )
80
+ const modalTitle = computed(() => {
81
+ const titleParts = [t(fieldRef.value.key)]
82
+ if (props.itemLabel && props.itemLabel !== "--") titleParts.push(props.itemLabel)
83
+ return titleParts.join(" - ")
84
+ })
85
+ const modalStyle = computed(() => ({
86
+ width: isAssetField.value ? "min(1200px, 96vw)" : "min(1000px, 95vw)",
87
+ }))
88
+
89
+ const inputValue = ref(currentValue.value)
90
+ const inputRef = ref()
91
+ const inputField = computed(() => ({
92
+ ...fieldRef.value,
93
+ subType: fieldRef.value.subType
94
+ ? ["radio", "checkbox"].includes(fieldRef.value.subType)
95
+ ? "select"
96
+ : fieldRef.value.subType
97
+ : undefined,
98
+ labelProps: { showLabel: false, showFeedback: false },
99
+ inputProps: {
100
+ show: true,
101
+ round: true,
102
+ ref: inputRef,
103
+ onBlur: isModalEdit.value ? undefined : handleChange,
104
+ },
105
+ }))
106
+ const isEdit = ref(false)
107
+ const instance = getCurrentInstance()
108
+ const activeEditKey = useState<string | null>("activeTableEditKey", () => null)
109
+ const editorKey = computed(
110
+ () =>
111
+ editKeyRef.value ??
112
+ `${fieldRef.value.id ?? fieldRef.value.key ?? "field"}-${instance?.uid ?? 0}`,
113
+ )
114
+
115
+ watch(
116
+ () => props.modelValue,
117
+ (value) => {
118
+ if (!isEdit.value) inputValue.value = value
119
+ },
120
+ )
121
+
122
+ watch(activeEditKey, (newKey) => {
123
+ if (isEdit.value && newKey !== editorKey.value) handleChange()
124
+ })
125
+
126
+ onMounted(() => {
127
+ isTouchDevice.value =
128
+ "ontouchstart" in window || navigator.maxTouchPoints > 0
129
+ document.addEventListener("keydown", handleKeydown)
130
+ })
131
+
132
+ function clearLongPress() {
133
+ if (longPressTimeout.value) {
134
+ clearTimeout(longPressTimeout.value)
135
+ longPressTimeout.value = null
136
+ }
137
+ }
138
+
139
+ function startEdit() {
140
+ inputValue.value = currentValue.value
141
+ activeEditKey.value = editorKey.value
142
+ isEdit.value = true
143
+
144
+ if (isModalEdit.value) return
145
+
146
+ setTimeout(() => {
147
+ if (inputRef.value) {
148
+ if (inputRef.value.focusInput) return inputRef.value.focusInput()
149
+ inputRef.value.focus()
150
+ }
151
+ }, 100)
152
+ }
153
+
154
+ function onContextmenu(e: MouseEvent | TouchEvent) {
155
+ if (!isEditable.value || isEdit.value) return
156
+
157
+ e.preventDefault()
158
+ e.stopPropagation()
159
+ clearLongPress()
160
+ startEdit()
161
+ }
162
+
163
+ function handleTouchStart(e: TouchEvent) {
164
+ if (!isTouchDevice.value || !isEditable.value || isEdit.value) return
165
+
166
+ clearLongPress()
167
+ longPressTimeout.value = window.setTimeout(() => {
168
+ onContextmenu(e)
169
+ }, 450)
170
+ }
171
+
172
+ onBeforeUnmount(() => {
173
+ clearLongPress()
174
+ document.removeEventListener("keydown", handleKeydown)
175
+ if (activeEditKey.value === editorKey.value) activeEditKey.value = null
176
+ })
177
+
178
+ function finishEdit(save = true) {
179
+ clearLongPress()
180
+ if (save && inputValue.value !== currentValue.value)
181
+ currentValue.value = inputValue.value
182
+ else inputValue.value = currentValue.value
183
+ isEdit.value = false
184
+ tooltipShow.value = false
185
+ if (activeEditKey.value === editorKey.value) activeEditKey.value = null
186
+ }
187
+
188
+ function handleKeydown(e: KeyboardEvent) {
189
+ if (
190
+ !isEdit.value ||
191
+ !((e.ctrlKey || e.metaKey) && (e.key.toLowerCase() === "s" || e.key === "س"))
192
+ )
193
+ return
194
+
195
+ e.preventDefault()
196
+ handleChange()
197
+ }
198
+
199
+ function cancelEdit() {
200
+ finishEdit(false)
201
+ }
202
+
203
+ function handleChange() {
204
+ finishEdit(true)
205
+ }
206
+ </script>
207
+
208
+ <style scoped>
209
+ .editable,
210
+ .editable * {
211
+ cursor: context-menu !important;
212
+ touch-action: manipulation;
213
+ -webkit-touch-callout: none;
214
+ }
215
+ </style>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <NA :href="`mailto:${value}`" target="_blank">
3
+ {{ value }}
4
+ </NA>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ const { value } = defineProps<{ value?: string }>()
9
+ </script>
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <NButton circle @click="showModal = true" :disabled="!valueRef || valueRef.length < 8" size="small"
3
+ @mouseover="buttonHovered = true" @mouseout="buttonHovered = false">
4
+ <template #icon>
5
+ <NIcon>
6
+ <Icon name="tabler:zoom-in" v-if="buttonHovered" />
7
+ <Icon name="tabler:brackets-angle" v-else />
8
+ </NIcon>
9
+ </template>
10
+ </NButton>
11
+ <NModal v-model:show="showModal" :title="modalTitle" draggable preset="card" style="width: 600px;">
12
+ <div v-html="valueRef?.replaceAll('<img ', '<img style=\'width:100%\'') || ''"></div>
13
+ </NModal>
14
+ </template>
15
+
16
+ <script lang="ts" setup>
17
+ const props = defineProps<{ field: Field; value?: string; itemLabel?: string }>()
18
+ const fieldRef = toRef(props, "field")
19
+ const valueRef = toRef(props, "value")
20
+ const showModal = ref(false)
21
+ const buttonHovered = ref(false)
22
+ const modalTitle = computed(() => {
23
+ const titleParts = [t(fieldRef.value.key)]
24
+ if (props.itemLabel && props.itemLabel !== "--") titleParts.push(props.itemLabel)
25
+ return titleParts.join(" - ")
26
+ })
27
+ </script>
@@ -0,0 +1,20 @@
1
+ <template>
2
+ <NFlex :wrap="false" align="center" :size="5">
3
+ <NIcon v-if="value" style="cursor: pointer;" size="medium" @click="copyID">
4
+ <Icon name="tabler:copy" />
5
+ </NIcon>
6
+ <NPerformantEllipsis>
7
+ {{ value }}
8
+ </NPerformantEllipsis>
9
+ </NFlex>
10
+ </template>
11
+
12
+ <script lang="ts" setup>
13
+ const { value } = defineProps<{ value?: string | number }>()
14
+
15
+ async function copyID() {
16
+ if (!value) return
17
+ await copyToClipboard(value)
18
+ window.$message.success(t("copiedSuccessfully"))
19
+ }
20
+ </script>
@@ -0,0 +1,32 @@
1
+ <template>
2
+ <NFlex :wrap="false" class="localeCell">
3
+ <NTag v-for="singleValue in ([] as string[]).concat(value)" :key="singleValue" round
4
+ :bordered="false">
5
+ {{ t(`languages.${singleValue}`) }}
6
+ </NTag>
7
+ </NFlex>
8
+ </template>
9
+
10
+ <script lang="ts" setup>
11
+ defineProps<{
12
+ value: string | number | (string | number)[]
13
+ }>()
14
+ </script>
15
+
16
+ <style scoped>
17
+ .localeCell {
18
+ max-width: 100%;
19
+ overflow: hidden;
20
+ min-width: 0;
21
+ }
22
+
23
+ .localeCell :deep(.n-tag) {
24
+ max-width: 100%;
25
+ }
26
+
27
+ .localeCell :deep(.n-tag__content) {
28
+ overflow: hidden;
29
+ text-overflow: ellipsis;
30
+ white-space: nowrap;
31
+ }
32
+ </style>
@@ -0,0 +1,18 @@
1
+ <template>
2
+ <NPopover scrollable style="max-height: 240px;max-width: 200px;" trigger="click">
3
+ <template #trigger>
4
+ <NButton size="small" circle>{{ `\{${Object.keys(value).length}\}` }}</NButton>
5
+ </template>
6
+ <NFlex vertical>
7
+ <NFlex v-for="child in (field.children as Schema).filter(({ key }) => typeof value[key] !== 'undefined')"
8
+ align="center" inline>
9
+ <strong>{{ t(child.key) }}:</strong>
10
+ <LazyColumn :field="child" :value="value[child.key]" />
11
+ </NFlex>
12
+ </NFlex>
13
+ </NPopover>
14
+ </template>
15
+
16
+ <script lang="ts" setup>
17
+ const { field, value } = defineProps<{ field: Field; value?: any }>()
18
+ </script>
@@ -0,0 +1,7 @@
1
+ <template>
2
+ •••••••
3
+ </template>
4
+
5
+ <script lang="ts" setup>
6
+ defineProps<{ value?: string }>()
7
+ </script>
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <NTag round :bordered="false">
3
+ <template #icon>
4
+ <span v-if="role" style="padding: 0 5px;">{{ t(role).charAt(0).toUpperCase() }}</span>
5
+ <Icon v-else name="question-mark" />
6
+ </template>
7
+ {{ role ? t(role) : "--" }}
8
+ </NTag>
9
+ </template>
10
+
11
+ <script lang="ts" setup>
12
+ const { value } = defineProps<{ value: string }>()
13
+ const database = useState<Database>("database")
14
+ const role = database.value.roles?.find(({ id }) => id === value)?.name
15
+ </script>
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <Column v-for="field of schema" :field="field" v-model="modelValue" />
3
+ </template>
4
+
5
+ <script lang="ts" setup>
6
+ // TO-DO:
7
+ // Add fields: Mention, Range, Slider
8
+
9
+ const { schema } = defineProps<{ schema?: Schema }>()
10
+
11
+ const modelValue = defineModel({
12
+ default: {},
13
+ })
14
+ </script>
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <NButton v-for="value in values" tag="a"
3
+ :href="`${$route.params.database ? `/${$route.params.database}` : ''}/admin/tables/${field.table}/${idOf(value)}`"
4
+ @click.prevent.stop="handleClick(value)" :loading="Loading[`Drawer_${field.table}_${idOf(value)}`]" size="small"
5
+ round>
6
+ <template v-if="table" #icon>
7
+ <LazyTableIcon :table="table" />
8
+ </template>
9
+ {{ renderLabel(table, resolvedItem(value) ?? ({ id: idOf(value) } as Item)) }}
10
+ </NButton>
11
+ </template>
12
+
13
+ <script lang="ts" setup>
14
+ import { isObject } from "inibase/utils";
15
+
16
+ const props = defineProps<{ field: Field; values: unknown[] }>();
17
+
18
+ const database = useState<Database>("database");
19
+ const table = database.value.tables?.find(({ slug }) => slug === props.field.table);
20
+ const Loading = useState<Record<string, boolean>>("Loading", () => ({}));
21
+
22
+ // Table references can be raw ids (translated / stored values). Resolve them
23
+ // through the shared cache which batches all ids of the current page into a
24
+ // single request per referenced table. Object values are used as-is and never
25
+ // trigger a fetch.
26
+ const referenced = useReferencedItems(props.field.table);
27
+
28
+ watch(
29
+ () => props.values,
30
+ (values) => referenced.register(values),
31
+ { immediate: true },
32
+ );
33
+
34
+ function idOf(value: unknown): string {
35
+ return isObject(value) ? String((value as Item).id ?? "") : String(value);
36
+ }
37
+
38
+ function resolvedItem(value: unknown): Item | undefined {
39
+ if (isObject(value)) return value as Item;
40
+ return referenced.items.value[idOf(value)];
41
+ }
42
+
43
+ async function handleClick(value: unknown) {
44
+ const id = idOf(value);
45
+ if (id && props.field.table) {
46
+ openDrawer(props.field.table, id, {}, "view");
47
+ }
48
+ }
49
+ </script>
@@ -0,0 +1,42 @@
1
+ <template>
2
+ <NButtonGroup v-if="values.length > 1" class="tableGroup">
3
+ <LazyColumnTableSingle :field="field" :values="[firstValue]" />
4
+
5
+ <NPopover style="max-height: 240px" scrollable>
6
+ <template #trigger>
7
+ <NButton size="small">
8
+ +{{ values.length - 1 }}
9
+ </NButton>
10
+ </template>
11
+ <NFlex vertical>
12
+ <LazyColumnTableSingle :field="field" :values="restValues" />
13
+ </NFlex>
14
+ </NPopover>
15
+ </NButtonGroup>
16
+
17
+ <LazyColumnTableSingle v-else :field="field" :values="values" />
18
+ </template>
19
+
20
+ <script lang="ts" setup>
21
+ const props = defineProps<{
22
+ field: Field;
23
+ value: Item | Item[] | string | number | (string | number)[];
24
+ }>();
25
+ // Computed (not setup-time constants): the grid replaces `value` when the row
26
+ // data reloads on language switch, and this component instance is patched in
27
+ // place rather than remounted — stale values made the cell keep showing the
28
+ // previous language's reference.
29
+ const values = computed(() => ([] as unknown[]).concat(props.value));
30
+ const firstValue = computed(() => values.value[0]);
31
+ const restValues = computed(() => values.value.slice(1));
32
+
33
+ // Register every id of the cell up front (including the +N popover rest) so the
34
+ // whole page's references land in the single batched fetch.
35
+ const referenced = useReferencedItems(props.field.table);
36
+
37
+ watch(
38
+ () => props.value,
39
+ (value) => referenced.register(value),
40
+ { immediate: true },
41
+ );
42
+ </script>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <NFlex :wrap="false" class="tagsCell">
3
+ <NTag v-for="singleValue in ([] as string[]).concat(value)" round :bordered="false"
4
+ :color="isArrayOfArrays(field.options) ? getColorObj(singleValue, field.options as [string | number, string][]) : undefined">
5
+ {{ singleValue }}</NTag>
6
+ </NFlex>
7
+ </template>
8
+
9
+ <script lang="ts" setup>
10
+ import { isArrayOfArrays } from "inibase/utils"
11
+
12
+ defineProps<{ field: Field; value: string | number | (string | number)[] }>()
13
+ </script>
14
+
15
+ <style scoped>
16
+ .tagsCell {
17
+ max-width: 100%;
18
+ overflow: hidden;
19
+ min-width: 0;
20
+ }
21
+
22
+ .tagsCell :deep(.n-tag) {
23
+ max-width: 100%;
24
+ }
25
+
26
+ .tagsCell :deep(.n-tag__content) {
27
+ overflow: hidden;
28
+ text-overflow: ellipsis;
29
+ white-space: nowrap;
30
+ }
31
+ </style>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <NPerformantEllipsis>
3
+ {{ value }}
4
+ </NPerformantEllipsis>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ defineProps<{ value?: string | number }>()
9
+ </script>