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,296 @@
1
+ <template>
2
+ <NGrid :x-gap="12" :y-gap="12" cols="1 500:2 800:4">
3
+ <NGridItem v-for="table in filteredTables" :key="table.slug">
4
+ <NCard hoverable>
5
+ <template #header>
6
+ <NuxtLink :to="getTableUrl(table.slug)">
7
+ <NFlex align="center">
8
+ <NIconWrapper :border-radius="50" style="font-style: normal">
9
+ <LazyTableIcon :table="table" />
10
+ </NIconWrapper>
11
+ <NH4 style="margin: 0">{{ t(table.slug) }}</NH4>
12
+ </NFlex>
13
+ </NuxtLink>
14
+ </template>
15
+ <template #header-extra>
16
+ <NDropdown :options="getDropdownOptions(table)" :renderLabel="renderDropdownLabel">
17
+ <NButton secondary size="small">
18
+ <template #icon>
19
+ <NIcon>
20
+ <Icon name="tabler:dots" />
21
+ </NIcon>
22
+ </template>
23
+ </NButton>
24
+ </NDropdown>
25
+ </template>
26
+ </NCard>
27
+ </NGridItem>
28
+ <NGridItem v-if="user?.role === config.public.idOne">
29
+ <NCard hoverable>
30
+ <template #header>
31
+ <NuxtLink :to="getTableUrl('backups')">
32
+ <NFlex align="center">
33
+ <NIconWrapper :border-radius="50" style="font-style: normal">
34
+ <NIcon>
35
+ <Icon name="tabler:database-export" />
36
+ </NIcon>
37
+ </NIconWrapper>
38
+ <NH4 style="margin: 0">{{ t("backups") }}</NH4>
39
+ </NFlex>
40
+ </NuxtLink>
41
+ </template>
42
+ </NCard>
43
+ </NGridItem>
44
+ <NGridItem v-if="user?.role === config.public.idOne">
45
+ <NPopover placement="bottom">
46
+ <template #trigger>
47
+ <NPopover trigger="click" v-model:show="showPopover">
48
+ <template #trigger>
49
+ <NCard style="cursor: pointer" content-style="padding: 15px 0" hoverable
50
+ @click="showPopover = !showPopover">
51
+ <NFlex justify="center" align="center">
52
+ <NIcon size="36">
53
+ <Icon name="tabler:plus" />
54
+ </NIcon>
55
+ </NFlex>
56
+ </NCard>
57
+ </template>
58
+ <NFlex vertical>
59
+ <NInputGroup>
60
+ <NInput v-model:value="newTableSlug" @keydown.enter.prevent="createTable"
61
+ :placeholder="t('tableSlug')">
62
+ <template #suffix>
63
+ <NIcon>
64
+ <Icon name="tabler:letter-case" />
65
+ </NIcon>
66
+ </template>
67
+ </NInput>
68
+ <NTooltip :delay="600">
69
+ <template #trigger>
70
+ <NButton tertiary :type="showQuickSettings ? 'primary' : 'default'"
71
+ @click="toggleQuickSettings">
72
+ <template #icon>
73
+ <NIcon>
74
+ <Icon v-if="showQuickSettings" name="tabler:settings-off" />
75
+ <Icon v-else name="tabler:settings" />
76
+ </NIcon>
77
+ </template>
78
+ </NButton>
79
+ </template>
80
+ {{ t('quickSettings') }}
81
+ </NTooltip>
82
+ <NButton secondary type="primary" @click="createTable" :loading="Loading.Table">
83
+ <template #icon>
84
+ <NIcon>
85
+ <Icon name="tabler:chevron-right" />
86
+ </NIcon>
87
+ </template>
88
+ </NButton>
89
+ </NInputGroup>
90
+ <template v-if="showQuickSettings">
91
+ <NDivider style="margin: 0" />
92
+ <NCheckbox v-model:checked="quickSettingsPrivate">
93
+ {{ t('makeItPrivate') }}
94
+ </NCheckbox>
95
+ <template v-if="!quickSettingsPrivate">
96
+ <NFlex :reverse="Language === 'ar'" v-for="role of modelValue.roles" :key="role.id" align="center"
97
+ justify="space-between" :wrap="false">
98
+ <NText strong>{{ t(role.name) }} :</NText>
99
+ <NCheckboxGroup v-model:value="rolePermissions[role.id]" :disabled="role.id === config.public.idOne">
100
+ <NCheckbox value="c" :label="t('create')" />
101
+ <NCheckbox value="r" :label="t('read')" />
102
+ <NCheckbox value="u" :label="t('update')" />
103
+ <NCheckbox value="d" :label="t('delete')" />
104
+ </NCheckboxGroup>
105
+ </NFlex>
106
+ </template>
107
+ </template>
108
+ </NFlex>
109
+ </NPopover>
110
+ </template>
111
+ {{ t('newTable') }}
112
+ </NPopover>
113
+ </NGridItem>
114
+ </NGrid>
115
+ </template>
116
+
117
+ <script lang="ts" setup>
118
+ import { type DropdownOption, NIcon } from "naive-ui"
119
+ import { Icon, NuxtLink } from "#components"
120
+
121
+ const modelValue = defineModel<Database>({ required: true })
122
+
123
+ const showPopover = ref(false)
124
+
125
+ const config = useRuntimeConfig()
126
+ const Loading = useState<Record<string, boolean>>("Loading", () => ({}))
127
+ const user = useState<User>("user")
128
+ const newTableSlug = ref()
129
+ const route = useRoute()
130
+
131
+ const database = useState<Database>("database")
132
+
133
+ function getTableUrl(slug: string) {
134
+ return `/${!route.params.database && database.value?.slug !== "inicontent" ? "" : `${modelValue.value.slug}/`}admin/tables/${slug}`
135
+ }
136
+
137
+ const Language = useScopedCookie<LanguagesType>("language", database.value?.slug)
138
+
139
+ const sessionID = useScopedCookie<string>("sid", database.value?.slug)
140
+
141
+ type CrudPermission = "c" | "r" | "u" | "d"
142
+ const permissionMethods: Record<CrudPermission, string> = {
143
+ c: "POST",
144
+ r: "GET",
145
+ u: "PUT",
146
+ d: "DELETE",
147
+ }
148
+
149
+ const showQuickSettings = ref(false)
150
+ const quickSettingsPrivate = ref(false)
151
+ const rolePermissions = reactive<Record<string, CrudPermission[]>>({})
152
+
153
+ function toggleQuickSettings() {
154
+ showQuickSettings.value = !showQuickSettings.value
155
+ if (showQuickSettings.value){
156
+ for (const { id } of modelValue.value.roles ?? [])
157
+ rolePermissions[id] = ["c", "r", "u", "d"]
158
+ } else resetQuickSettings()
159
+ }
160
+
161
+ function resetQuickSettings() {
162
+ showQuickSettings.value = false
163
+ quickSettingsPrivate.value = false
164
+ for (const id of Object.keys(rolePermissions)) delete rolePermissions[id]
165
+ }
166
+
167
+ // "@user.4" is the user role field, role ids come from database.roles
168
+ function generateQuickSettingsFlows(): FlowType[] {
169
+ if (quickSettingsPrivate.value) return [[["error", "accessDenied"]]]
170
+
171
+ // if no change made to role permissions, no need to create flows
172
+ if (Object.values(rolePermissions).filter(permissions => permissions.length === 4).length === modelValue.value.roles?.length)
173
+ return []
174
+
175
+ const flows: FlowType[] = []
176
+ for (const { id } of modelValue.value.roles ?? []) {
177
+ // skip flows for admin
178
+ if (id === config.public.idOne) continue;
179
+
180
+ const allowedPermissions = rolePermissions[id]
181
+ if (!allowedPermissions || allowedPermissions.length === 4) continue
182
+ const deniedMethods = (
183
+ Object.keys(permissionMethods) as CrudPermission[]
184
+ )
185
+ .filter((permission) => !allowedPermissions.includes(permission))
186
+ .map((permission) => permissionMethods[permission])
187
+ if (!deniedMethods.length) continue
188
+ const flow: FlowType = [["@user.4", "=", id]]
189
+ if (deniedMethods.length < Object.keys(permissionMethods).length)
190
+ flow.push(["@method", "[]", deniedMethods])
191
+ flow.push(["error", "accessDenied"])
192
+ flows.push(flow)
193
+ }
194
+ return flows
195
+ }
196
+
197
+ const createTable = async () => {
198
+ if (newTableSlug.value) {
199
+ Loading.value.Table = true
200
+
201
+ const bodyContent: string = toRaw(newTableSlug.value)
202
+ const quickSettingsFlows = generateQuickSettingsFlows()
203
+
204
+ const data = await $fetch<apiResponse<Table>>(
205
+ `${config.public.apiBase}inicontent/databases/${modelValue.value.slug}/${bodyContent}`,
206
+ {
207
+ method: "POST",
208
+ body: quickSettingsFlows.length ? { onRequest: quickSettingsFlows } : undefined,
209
+ params: {
210
+ locale: Language.value,
211
+ [`${database.value.slug}_sid`]: sessionID.value,
212
+ },
213
+ credentials: "include",
214
+ },
215
+ )
216
+
217
+ if (data.result) {
218
+ modelValue.value.tables?.push(data.result)
219
+ window.$message.success(data.message)
220
+ newTableSlug.value = null
221
+ showPopover.value = false
222
+ } else window.$message.error(data.message ?? t("error"))
223
+
224
+ Loading.value.Table = false
225
+ } else window.$message.error(t("inputsAreInvalid"))
226
+ }
227
+
228
+ const filteredTables = computed(() =>
229
+ modelValue.value.tables
230
+ ?.filter(
231
+ ({ allowedMethods, show, slug }) =>
232
+ allowedMethods?.includes("r") && show !== false && slug !== "dashboards",
233
+ )
234
+ .sort(
235
+ (a, b) =>
236
+ Number(
237
+ ["users", "sessions", "translations", "assets"].includes(b.slug),
238
+ ) -
239
+ Number(
240
+ ["users", "sessions", "translations", "assets"].includes(a.slug),
241
+ ),
242
+ ),
243
+ )
244
+
245
+ const getDropdownOptions = (table: Table) => {
246
+ const tableUrl = getTableUrl(table.slug)
247
+ return [
248
+ {
249
+ key: tableUrl,
250
+ label: t("showAll"),
251
+ icon: () => h(NIcon, () => h(Icon, { name: "tabler:eye" })),
252
+ },
253
+ {
254
+ key: `${tableUrl}/new`,
255
+ label: t("newItem"),
256
+ icon: () => h(NIcon, () => h(Icon, { name: "tabler:plus" })),
257
+ show: table.slug !== "assets" && table.allowedMethods?.includes("c"),
258
+ },
259
+ {
260
+ key: `${tableUrl}/settings`,
261
+ label: t("settings"),
262
+ icon: () => h(NIcon, () => h(Icon, { name: "tabler:settings" })),
263
+ show:
264
+ !["sessions", "translations", "assets"].includes(table.slug) &&
265
+ user.value?.role === config.public.idOne,
266
+ },
267
+ {
268
+ key: `${tableUrl}/flows`,
269
+ label: t("flows"),
270
+ icon: () => h(NIcon, () => h(Icon, { name: "tabler:webhook" })),
271
+ show:
272
+ !["sessions", "translations"].includes(table.slug) &&
273
+ user.value?.role === config.public.idOne,
274
+ },
275
+ {
276
+ key: `${tableUrl}/schedules`,
277
+ label: t("schedules"),
278
+ icon: () => h(NIcon, () => h(Icon, { name: "tabler:clock-play" })),
279
+ show:
280
+ !["sessions", "translations", "assets"].includes(table.slug) &&
281
+ table.allowedMethods?.includes("c") &&
282
+ user.value?.role === config.public.idOne,
283
+ },
284
+ ]
285
+ }
286
+
287
+ function renderDropdownLabel(option: DropdownOption) {
288
+ return h(
289
+ NuxtLink,
290
+ {
291
+ to: option.key as string,
292
+ },
293
+ () => option.label,
294
+ )
295
+ }
296
+ </script>
@@ -0,0 +1,36 @@
1
+ <template>
2
+ <NIcon>
3
+ <Icon
4
+ v-if="table.icon || ['assets', 'translations', 'users', 'sessions', 'pages', 'blocks', 'templates'].includes(table.slug)"
5
+ :name="getTableIcon(table)" />
6
+ <span v-else>{{ t(table.slug).charAt(0).toUpperCase() }}</span>
7
+ </NIcon>
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ defineProps<{
12
+ table: Table
13
+ }>()
14
+
15
+ function getTableIcon(table: Table) {
16
+ if (table.icon) return `tabler:${table.icon}`
17
+ switch (table.slug) {
18
+ case "assets":
19
+ return "tabler:folder"
20
+ case "translations":
21
+ return "tabler:language"
22
+ case "users":
23
+ return "tabler:users"
24
+ case "sessions":
25
+ return "tabler:fingerprint"
26
+ case "pages":
27
+ return "tabler:template"
28
+ case "blocks":
29
+ return "tabler:layout-2"
30
+ case "templates":
31
+ return "tabler:mail-spark"
32
+ default:
33
+ return "tabler:table"
34
+ }
35
+ }
36
+ </script>
@@ -0,0 +1,181 @@
1
+ <template>
2
+ <NCollapse v-if="hasLogs" :default-expanded-names="isOpen ? ['logs'] : []" @item-header-click="handleCollapseChange">
3
+ <NCollapseItem name="logs">
4
+ <template #header>
5
+ <NH4 style="margin: 0">{{ t('latestActivities') }}</NH4>
6
+ </template>
7
+ <NSpin :show="Loading.logs">
8
+ <NScrollbar v-if="data?.result" style="max-height: 340px">
9
+ <NTimeline :item-placement="Language === 'ar' ? 'right' : 'left'">
10
+ <NTimelineItem v-for="log in data.result?.filter((log) => log.actions?.length)" :key="log.id"
11
+ :type="getTypeFromAction((log.actions[0] as Actions[0])[0])">
12
+ <template #header>
13
+ <NFlex align="center" size="small">
14
+ <NText type="primary">{{ renderLabel(usersTable, log.madeBy) }}</NText>
15
+ <template v-if="(log.actions[0] as Actions[0])[0] === 'create'">
16
+ {{ t('created') }} {{ t("newItem") }}:
17
+ <NButtonGroup>
18
+ <NButton v-for="item in ([] as Item[]).concat(log.item)" round secondary
19
+ size="small" type="primary">
20
+ {{ renderLabel(table, item, `[${t('removed')}]`) }}
21
+ </NButton>
22
+ </NButtonGroup>
23
+ </template>
24
+ <template v-else-if="(log.actions[0] as Actions[0])[0] === 'delete'">
25
+ {{ t('deleted') }} {{ t("anItem") }}
26
+ </template>
27
+ <template v-else>
28
+ {{ t('updated') }}
29
+ <NButtonGroup>
30
+ <NButton v-for="item in ([] as Item[]).concat(log.item)" round secondary
31
+ size="small" type="primary">{{ renderLabel(table, item) }}
32
+ </NButton>
33
+ </NButtonGroup>
34
+ {{ t('asFollow') }}:
35
+ </template>
36
+ </NFlex>
37
+ </template>
38
+ <NFlex vertical style="line-height: 2.5;">
39
+ <template v-for="action in log.actions">
40
+ <NText v-if="action[0] === 'unset'">{{ t(action[0]) }} <NTag :bordered="false" round
41
+ type="warning">{{
42
+ t(action[1]) }}</NTag>
43
+ </NText>
44
+ <NText v-else-if="action[0] === 'remove'">{{ t(action[0]) }} {{ t("from") }}
45
+ <NTag :bordered="false" round type="error">{{ t(action[1]) }}</NTag>
46
+ </NText>
47
+ <NText v-else-if="action[0] === 'add'">{{ t(action[0]) }} {{ t("to") }} <NTag
48
+ :bordered="false" round type="success">{{ t(action[1]) }}</NTag>
49
+ </NText>
50
+ <NText v-else-if="action[0] === 'set'">{{ t(action[0]) }} <NTag :bordered="false"
51
+ round type="info">{{ t(action[1]) }}</NTag> {{ t("as") }} <NTag
52
+ :bordered="false" round>{{ t(action[2]) }}
53
+ </NTag>
54
+ </NText>
55
+ <NText v-else-if="action[0] === 'update'">{{ t(action[0]) }} <NTag :bordered="false"
56
+ round type="info">
57
+ {{ t(action[1]) }}</NTag> {{
58
+ t("to") }}
59
+ <NTag :bordered="false" round>{{ t(action[2]) }}</NTag>
60
+ </NText>
61
+ </template>
62
+ </NFlex>
63
+ <template #footer>
64
+ <NTime :time="log.createdAt" type="relative" />
65
+ </template>
66
+ </NTimelineItem>
67
+ </NTimeline>
68
+ </NScrollbar>
69
+ </NSpin>
70
+ </NCollapseItem>
71
+ </NCollapse>
72
+ </template>
73
+
74
+ <script lang="ts" setup>
75
+ import Inison from "inison"
76
+
77
+ type ActionName =
78
+ | "set"
79
+ | "add"
80
+ | "remove"
81
+ | "update"
82
+ | "unset"
83
+ | "delete"
84
+ | "create"
85
+ type Actions = [ActionName, string, any?][]
86
+ interface Log extends Item {
87
+ actions: Actions
88
+ madeBy?: User
89
+ item: Item | Item[]
90
+ }
91
+
92
+ const { id, open } = defineProps<{
93
+ id?: string | number
94
+ open?: boolean
95
+ }>()
96
+ const isOpen = ref(open)
97
+
98
+ const config = useRuntimeConfig()
99
+ const Loading = useState<Record<string, boolean>>("Loading", () => ({}))
100
+ const database = useState<Database>("database");
101
+ const Language = useScopedCookie<LanguagesType>("language", database.value?.slug)
102
+
103
+ const usersTable = database.value.tables?.find(({ slug }) => slug === "users")
104
+
105
+ const table = useState<Table>("table")
106
+
107
+ onBeforeRouteLeave(() => {
108
+ clearNuxtData(`${database.value.slug}/${table.value?.slug as string}/logs`)
109
+ })
110
+
111
+ const sessionID = useScopedCookie<string>("sid", database.value?.slug)
112
+ const hasLogs = ref(true)
113
+
114
+ const { data, execute } = await useLazyFetch<apiResponse<Log[]>>(
115
+ () => `${config.public.apiBase}${database.value.slug}/${table.value.slug}/logs`,
116
+ {
117
+ query: {
118
+ where: id ? `{item:${id}}` : undefined,
119
+ options: Inison.stringify({
120
+ columns: [
121
+ "*",
122
+ ...(table.value.columns?.map((column) => `item.${column}`) ?? []),
123
+ ...(usersTable?.columns?.map((column) => `madeBy.${column}`) ?? []),
124
+ ],
125
+ }),
126
+ locale: Language,
127
+ [`${database.value.slug}_sid`]: sessionID.value,
128
+ },
129
+ immediate: false,
130
+ onRequest() {
131
+ Loading.value.logs = true
132
+ },
133
+ onResponse: ({ response: { _data } }) => {
134
+ Loading.value.logs = false
135
+ hasLogs.value = !!_data?.result?.length
136
+ },
137
+ credentials: "include",
138
+ },
139
+ )
140
+
141
+ let firstTime = true
142
+ watch(
143
+ isOpen,
144
+ (newValue) => {
145
+ if (newValue && firstTime) {
146
+ execute()
147
+ firstTime = false
148
+ }
149
+ },
150
+ { immediate: true },
151
+ )
152
+
153
+ // Refetch logs when the current language changes (log messages are
154
+ // locale-dependent), but only once they've been loaded at least once
155
+ watch(Language, () => {
156
+ if (!firstTime) execute()
157
+ })
158
+
159
+ function handleCollapseChange({
160
+ name,
161
+ expanded,
162
+ }: {
163
+ name: string | number
164
+ expanded: boolean
165
+ }) {
166
+ if (name === "logs") isOpen.value = expanded
167
+ }
168
+
169
+ function getTypeFromAction(
170
+ ActionName: ActionName,
171
+ ): "error" | "info" | "success" {
172
+ switch (ActionName) {
173
+ case "create":
174
+ return "success"
175
+ case "delete":
176
+ return "error"
177
+ default:
178
+ return "info"
179
+ }
180
+ }
181
+ </script>