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.
- package/.github/pull_request.yml +18 -0
- package/CONTRIBUTING.md +67 -0
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/app/app.vue +106 -0
- package/app/assets/main.css +974 -0
- package/app/components/Api/EndpointCard.vue +169 -0
- package/app/components/Api/SchemaDocsTable.vue +208 -0
- package/app/components/Asset/Card.vue +1363 -0
- package/app/components/Asset/Grid.vue +295 -0
- package/app/components/Asset/Icon.vue +38 -0
- package/app/components/Asset/Preview.vue +486 -0
- package/app/components/Asset/Scanner.vue +700 -0
- package/app/components/Asset/Thumb.vue +708 -0
- package/app/components/Auth/Reset.vue +99 -0
- package/app/components/Auth/index.vue +346 -0
- package/app/components/ChatInterface.vue +2326 -0
- package/app/components/ChatPagePreview.vue +34 -0
- package/app/components/Column/Array.vue +29 -0
- package/app/components/Column/Asset.vue +21 -0
- package/app/components/Column/Boolean.vue +12 -0
- package/app/components/Column/Color.vue +9 -0
- package/app/components/Column/Date.vue +57 -0
- package/app/components/Column/Edit.vue +215 -0
- package/app/components/Column/Email.vue +9 -0
- package/app/components/Column/Html.vue +27 -0
- package/app/components/Column/Id.vue +20 -0
- package/app/components/Column/Locale.vue +32 -0
- package/app/components/Column/Object.vue +18 -0
- package/app/components/Column/Password.vue +7 -0
- package/app/components/Column/Role.vue +15 -0
- package/app/components/Column/S.vue +14 -0
- package/app/components/Column/Table/Single.vue +49 -0
- package/app/components/Column/Table/index.vue +42 -0
- package/app/components/Column/Tags.vue +31 -0
- package/app/components/Column/Text.vue +9 -0
- package/app/components/Column/Url.vue +9 -0
- package/app/components/Column/index.vue +41 -0
- package/app/components/Dashboard/Editor.vue +221 -0
- package/app/components/Dashboard/Grid.vue +270 -0
- package/app/components/Dashboard/View.vue +131 -0
- package/app/components/Dashboard/Widget/BarChart.vue +73 -0
- package/app/components/Dashboard/Widget/Counter.vue +51 -0
- package/app/components/Dashboard/Widget/LineChart.vue +76 -0
- package/app/components/Dashboard/Widget/PieChart.vue +75 -0
- package/app/components/Dashboard/Widget/Table.vue +94 -0
- package/app/components/Dashboard/WidgetEditor.vue +274 -0
- package/app/components/Data/Array.vue +102 -0
- package/app/components/Data/Html.vue +9 -0
- package/app/components/Data/Icon.vue +9 -0
- package/app/components/Data/Object.vue +10 -0
- package/app/components/Data/S.vue +16 -0
- package/app/components/Data/Table.vue +53 -0
- package/app/components/Data/Value.vue +34 -0
- package/app/components/Data/index.vue +14 -0
- package/app/components/Field/Array.vue +324 -0
- package/app/components/Field/Asset/Actions.vue +97 -0
- package/app/components/Field/Asset/index.vue +639 -0
- package/app/components/Field/Boolean.vue +26 -0
- package/app/components/Field/Checkbox.vue +48 -0
- package/app/components/Field/Color.vue +27 -0
- package/app/components/Field/Date.vue +30 -0
- package/app/components/Field/Email.vue +51 -0
- package/app/components/Field/EmailTemplate.vue +420 -0
- package/app/components/Field/Html/ColorPicker.vue +127 -0
- package/app/components/Field/Html/Editor.vue +544 -0
- package/app/components/Field/Html/index.vue +25 -0
- package/app/components/Field/Icon.vue +114 -0
- package/app/components/Field/JSON.vue +113 -0
- package/app/components/Field/Mention.vue +36 -0
- package/app/components/Field/Number.vue +27 -0
- package/app/components/Field/Object.vue +92 -0
- package/app/components/Field/Password.vue +55 -0
- package/app/components/Field/Radio.vue +47 -0
- package/app/components/Field/S.vue +44 -0
- package/app/components/Field/Select.vue +49 -0
- package/app/components/Field/Table.vue +348 -0
- package/app/components/Field/Tags.vue +113 -0
- package/app/components/Field/Text.vue +31 -0
- package/app/components/Field/Textarea.vue +33 -0
- package/app/components/Field/Url.vue +40 -0
- package/app/components/Field/Wrapper.vue +128 -0
- package/app/components/Field/index.vue +68 -0
- package/app/components/FloatingChatbot.vue +73 -0
- package/app/components/Form/Card.vue +151 -0
- package/app/components/Form/Drawer.vue +195 -0
- package/app/components/Form/index.vue +621 -0
- package/app/components/Header.vue +328 -0
- package/app/components/Offline/ConflictModal.vue +200 -0
- package/app/components/Offline/SyncStatus.vue +249 -0
- package/app/components/PlatformFeatureLanding.vue +65 -0
- package/app/components/Table/Backups.vue +638 -0
- package/app/components/Table/Flows.vue +814 -0
- package/app/components/Table/Grid.vue +296 -0
- package/app/components/Table/Icon.vue +36 -0
- package/app/components/Table/Logs.vue +181 -0
- package/app/components/Table/Schedules.vue +722 -0
- package/app/components/Table/Search/Button.vue +256 -0
- package/app/components/Table/Search/Items.vue +286 -0
- package/app/components/Table/Search/index.vue +87 -0
- package/app/components/Table/Settings/Schema.vue +801 -0
- package/app/components/Table/Settings/index.vue +631 -0
- package/app/components/Table/TranslateDrawer.vue +526 -0
- package/app/components/Table/Views/Kanban.vue +289 -0
- package/app/components/Table/Views/Table.vue +1273 -0
- package/app/components/Table/index.vue +890 -0
- package/app/composables/Translation/define.ts +41 -0
- package/app/composables/Translation/fetch.ts +107 -0
- package/app/composables/Translation/languages.ts +1 -0
- package/app/composables/Translation/loadCoreTranslations.ts +38 -0
- package/app/composables/Translation/translate.ts +250 -0
- package/app/composables/databaseCookies.ts +79 -0
- package/app/composables/fieldValidator.ts +72 -0
- package/app/composables/fieldsList.ts +301 -0
- package/app/composables/formatDatabase.ts +101 -0
- package/app/composables/index.ts +444 -0
- package/app/composables/openDrawer.ts +118 -0
- package/app/composables/optimizeFile.ts +287 -0
- package/app/composables/renderLabel.ts +88 -0
- package/app/composables/schemaClipboard.ts +95 -0
- package/app/composables/search.ts +353 -0
- package/app/composables/setThemeConfig.ts +48 -0
- package/app/composables/translationValue.ts +169 -0
- package/app/composables/useAssetPreview.ts +84 -0
- package/app/composables/useAssetUploader.ts +56 -0
- package/app/composables/useDashboardData.ts +310 -0
- package/app/composables/useDocumentThumbnail.ts +351 -0
- package/app/composables/useEcharts.ts +33 -0
- package/app/composables/useLocalDatabaseConfig.ts +51 -0
- package/app/composables/useOcr.ts +300 -0
- package/app/composables/useOfflineFetch.ts +194 -0
- package/app/composables/useOfflineItem.ts +107 -0
- package/app/composables/useOfflineQueue.ts +302 -0
- package/app/composables/useOfflineSync.ts +235 -0
- package/app/composables/usePasskeyAuth.ts +156 -0
- package/app/composables/usePdfCompressor.ts +180 -0
- package/app/composables/usePlatformRedirect.ts +72 -0
- package/app/composables/useRealtimeSync.ts +233 -0
- package/app/composables/useReferencedItems.ts +180 -0
- package/app/composables/useScanner.ts +664 -0
- package/app/composables/useSubscription.ts +650 -0
- package/app/composables/useThumbnailCache.ts +136 -0
- package/app/composables/useVideoCompressor.ts +194 -0
- package/app/error.vue +112 -0
- package/app/layouts/api.vue +188 -0
- package/app/layouts/dashboard.vue +91 -0
- package/app/layouts/default.vue +228 -0
- package/app/layouts/table.vue +471 -0
- package/app/locales/ar.ts +996 -0
- package/app/locales/en.ts +655 -0
- package/app/locales/es.ts +996 -0
- package/app/locales/fr.ts +997 -0
- package/app/middleware/dashboard.ts +33 -0
- package/app/middleware/database.ts +83 -0
- package/app/middleware/global.ts +1 -0
- package/app/middleware/sid-ingest.global.ts +29 -0
- package/app/middleware/table.ts +54 -0
- package/app/middleware/user.ts +26 -0
- package/app/pages/[[database]]/admin/api/auth/index.vue +306 -0
- package/app/pages/[[database]]/admin/api/index.vue +18 -0
- package/app/pages/[[database]]/admin/api/tables/[table]/index.vue +497 -0
- package/app/pages/[[database]]/admin/api/tables/index.vue +87 -0
- package/app/pages/[[database]]/admin/billing/index.vue +16 -0
- package/app/pages/[[database]]/admin/dashboards/[id]/edit.vue +66 -0
- package/app/pages/[[database]]/admin/dashboards/[id]/index.vue +117 -0
- package/app/pages/[[database]]/admin/dashboards/index.vue +20 -0
- package/app/pages/[[database]]/admin/index.vue +28 -0
- package/app/pages/[[database]]/admin/settings.vue +526 -0
- package/app/pages/[[database]]/admin/tables/[table]/[id]/edit.vue +143 -0
- package/app/pages/[[database]]/admin/tables/[table]/[id]/index.vue +146 -0
- package/app/pages/[[database]]/admin/tables/[table]/flows.vue +10 -0
- package/app/pages/[[database]]/admin/tables/[table]/index.vue +10 -0
- package/app/pages/[[database]]/admin/tables/[table]/new.vue +12 -0
- package/app/pages/[[database]]/admin/tables/[table]/schedules.vue +10 -0
- package/app/pages/[[database]]/admin/tables/[table]/settings.vue +30 -0
- package/app/pages/[[database]]/admin/tables/assets/[...path].vue +24 -0
- package/app/pages/[[database]]/admin/tables/assets/flows.vue +10 -0
- package/app/pages/[[database]]/admin/tables/backups/index.vue +10 -0
- package/app/pages/[[database]]/admin/tables/blocks/index.vue +16 -0
- package/app/pages/[[database]]/admin/tables/index.vue +20 -0
- package/app/pages/[[database]]/admin/tables/pages/index.vue +16 -0
- package/app/pages/[[database]]/admin/tables/templates/index.vue +16 -0
- package/app/pages/[[database]]/api/index.vue +120 -0
- package/app/pages/[[database]]/auth/index.vue +12 -0
- package/app/pages/[[database]]/auth/reset.vue +10 -0
- package/app/pages/[[database]]/index.vue +10 -0
- package/app/plugins/offline-warmup.client.ts +53 -0
- package/app/plugins/template-globals.ts +9 -0
- package/app/spa-loading-template.html +93 -0
- package/biome.json +53 -0
- package/index.d.ts +380 -0
- package/modules/naiveui.ts +41 -0
- package/nuxt.config.ts +269 -0
- package/package.json +56 -0
- package/pnpm-workspace.yaml +5 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/pwa-192x192.png +0 -0
- package/public/pwa-512x512.png +0 -0
- package/public/pwa-maskable-192x192.png +0 -0
- package/public/pwa-maskable-512x512.png +0 -0
- package/public/vendor/opencv.js +48 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<NPageHeader>
|
|
3
|
+
<template #avatar>
|
|
4
|
+
<NuxtLink v-if="String($route.matched[0]?.name).startsWith('database')"
|
|
5
|
+
:to="`/${$route.params.database ?? ''}`">
|
|
6
|
+
<NTag strong round :bordered="false" style="cursor:pointer;font-weight:600"
|
|
7
|
+
:style="ThemeConfig.revert && Theme === 'dark' ? 'color:#000;background-color:#fff' : ''">
|
|
8
|
+
<template #avatar>
|
|
9
|
+
<NAvatar fallbackSrc="/favicon.ico" :style='{
|
|
10
|
+
backgroundColor: "transparent"
|
|
11
|
+
}' :src='database?.icon?.publicURL ?? "/favicon.ico"' />
|
|
12
|
+
</template>
|
|
13
|
+
{{ t(database?.slug ?? 'inicontent') }}
|
|
14
|
+
</NTag>
|
|
15
|
+
</NuxtLink>
|
|
16
|
+
<NTag v-else strong round :bordered="false">
|
|
17
|
+
<template #avatar>
|
|
18
|
+
<NAvatar fallbackSrc="/favicon.ico" :src='database?.icon?.publicURL ?? "/favicon.ico"' />
|
|
19
|
+
</template>
|
|
20
|
+
{{ t(database?.slug ?? 'inicontent') }}
|
|
21
|
+
</NTag>
|
|
22
|
+
</template>
|
|
23
|
+
<template #title v-if='showBreadcrumb'>
|
|
24
|
+
<NBreadcrumb>
|
|
25
|
+
<NBreadcrumbItem v-for="(_, index) of breadcrumbArray">
|
|
26
|
+
<NuxtLink :to="breadCrumbItemLink(index)">
|
|
27
|
+
{{ breadCrumbItemLabel(index) }}
|
|
28
|
+
</NuxtLink>
|
|
29
|
+
</NBreadcrumbItem>
|
|
30
|
+
</NBreadcrumb>
|
|
31
|
+
</template>
|
|
32
|
+
<template #extra>
|
|
33
|
+
<NButtonGroup>
|
|
34
|
+
<LazyOfflineSyncStatus show-pwa />
|
|
35
|
+
<NPopover v-if="user?.role === config.public.idOne" :delay="600" scrollable style="max-height: 240px;">
|
|
36
|
+
<template #trigger>
|
|
37
|
+
<NButton secondary round size="small">{{ humanFileSize(totalSize) }}</NButton>
|
|
38
|
+
</template>
|
|
39
|
+
<div v-if="barSegments.length > 0"
|
|
40
|
+
style="height: 6px; border-radius: 3px; overflow: hidden; display: flex; gap: 2px; background: rgba(128,128,128,.2); margin-block-end: 10px;">
|
|
41
|
+
<div v-for="table in barSegments" :key="table.slug"
|
|
42
|
+
:style="{ width: `${((table.size ?? 0) / totalSize) * 100}%`, backgroundColor: tableColor(table) }" />
|
|
43
|
+
</div>
|
|
44
|
+
<NFlex v-for="(table, index) in sortedTables" :key="table.slug" justify="space-between" align="center">
|
|
45
|
+
<NFlex align="center" :size="6">
|
|
46
|
+
<span :style="{
|
|
47
|
+
width: '8px',
|
|
48
|
+
height: '8px',
|
|
49
|
+
borderRadius: '50%',
|
|
50
|
+
// Tables under the 5% bar threshold have no segment, so
|
|
51
|
+
// their dot renders grey — signaling 'too small to draw'.
|
|
52
|
+
backgroundColor: barSegments.some((t) => t.slug === table.slug)
|
|
53
|
+
? tableColor(table)
|
|
54
|
+
: 'rgba(128,128,128,.5)',
|
|
55
|
+
flexShrink: 0,
|
|
56
|
+
}" />
|
|
57
|
+
<LazyTableIcon :table="table" />
|
|
58
|
+
<NText>{{ t(table.slug) }}</NText>
|
|
59
|
+
</NFlex>
|
|
60
|
+
<NText depth="3">{{ humanFileSize(table?.size) }}</NText>
|
|
61
|
+
</NFlex>
|
|
62
|
+
</NPopover>
|
|
63
|
+
<NDropdown v-if="languagesDropdownOptions?.length > 1" :value="Language" :options="languagesDropdownOptions"
|
|
64
|
+
@select="(v) => Language = v">
|
|
65
|
+
<NButton secondary round size="small">
|
|
66
|
+
<template #icon>
|
|
67
|
+
<NIcon>
|
|
68
|
+
<Icon name="tabler:language" />
|
|
69
|
+
</NIcon>
|
|
70
|
+
</template>
|
|
71
|
+
</NButton>
|
|
72
|
+
</NDropdown>
|
|
73
|
+
<NDropdown :options="userDropdownOptions" @select="onSelectUserDropdown">
|
|
74
|
+
<NButton secondary type="primary" round size="small">
|
|
75
|
+
<template #icon>
|
|
76
|
+
<NIcon>
|
|
77
|
+
<Icon name="tabler:user" />
|
|
78
|
+
</NIcon>
|
|
79
|
+
</template>
|
|
80
|
+
<template v-if="user">
|
|
81
|
+
<NText strong>{{ user.username.charAt(0).toUpperCase() +
|
|
82
|
+
user.username.slice(1) }}</NText>
|
|
83
|
+
</template>
|
|
84
|
+
</NButton>
|
|
85
|
+
</NDropdown>
|
|
86
|
+
</NButtonGroup>
|
|
87
|
+
</template>
|
|
88
|
+
</NPageHeader>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<script setup lang="ts">
|
|
92
|
+
import { isValidID } from "inibase/utils";
|
|
93
|
+
import { Icon, NIcon, NTag, NText, NFlex } from "#components";
|
|
94
|
+
|
|
95
|
+
const config = useRuntimeConfig();
|
|
96
|
+
const route = useRoute();
|
|
97
|
+
const user = useState<User | undefined>("user");
|
|
98
|
+
const database = useState<Database>("database");
|
|
99
|
+
|
|
100
|
+
const Language = useScopedCookie<LanguagesType>("language", database.value?.slug);
|
|
101
|
+
const Theme = useCookie<"light" | "dark">("theme", { sameSite: true });
|
|
102
|
+
const redirectTo = useScopedCookie<string | null>("redirectTo", database.value?.slug);
|
|
103
|
+
|
|
104
|
+
const ThemeConfig = useState<ThemeConfig>("ThemeConfig", () => ({
|
|
105
|
+
primaryColor: "#FF9800",
|
|
106
|
+
primaryColorHover: "#F7A42A",
|
|
107
|
+
primaryColorPressed: "#E19421",
|
|
108
|
+
primaryColorSuppl: "#CB7900",
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
const showBreadcrumb = computed(
|
|
112
|
+
() =>
|
|
113
|
+
database.value?.slug &&
|
|
114
|
+
!["index", "auth", "database-auth"].includes(
|
|
115
|
+
String(route.matched[0]?.name),
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const sortedTables = computed(() =>
|
|
120
|
+
[...(database.value?.tables ?? [])].sort(
|
|
121
|
+
(a, b) => (b.size ?? 0) - (a.size ?? 0),
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
const totalSize = computed(() =>
|
|
125
|
+
sortedTables.value.reduce((total, table) => total + (table.size ?? 0), 0),
|
|
126
|
+
);
|
|
127
|
+
// Only draw bar segments that represent a meaningful share of storage, so
|
|
128
|
+
// tiny tables don't render as sub-pixel slivers of clutter. The gap left at
|
|
129
|
+
// the end of the bar reads as "the remaining small tables".
|
|
130
|
+
const barSegments = computed(() =>
|
|
131
|
+
totalSize.value === 0
|
|
132
|
+
? []
|
|
133
|
+
: sortedTables.value.filter(
|
|
134
|
+
(table) => ((table.size ?? 0) / totalSize.value) * 100 >= 5,
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
// Spread hues evenly across however many tables exist, so colors never repeat
|
|
138
|
+
// (unlike a fixed palette that cycles once there are more tables than colors).
|
|
139
|
+
// Key color off the table's position in the FULL sorted list, so a row's dot
|
|
140
|
+
// and its bar segment always share the same hue — even after `barSegments`
|
|
141
|
+
// drops sub-5% tables (whose segment index would otherwise disagree with the
|
|
142
|
+
// row index). Hues spread evenly so they never repeat, regardless of count.
|
|
143
|
+
const tableColor = (table: Table) => {
|
|
144
|
+
const count = Math.max(sortedTables.value.length, 1);
|
|
145
|
+
const index = Math.max(
|
|
146
|
+
sortedTables.value.findIndex((t) => t.slug === table.slug),
|
|
147
|
+
0,
|
|
148
|
+
);
|
|
149
|
+
return `hsl(${(360 / count) * index} 70% 52%)`;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
function breadCrumbItemLink(index: number) {
|
|
153
|
+
const breadcrumbSegments = breadcrumbArray.value.slice(0, index + 1);
|
|
154
|
+
if (hideDatabaseInBreadcrumb.value && databaseParam.value)
|
|
155
|
+
return `/${databaseParam.value}/${breadcrumbSegments.join("/")}`;
|
|
156
|
+
return `/${breadcrumbSegments.join("/")}`;
|
|
157
|
+
}
|
|
158
|
+
const routeSegments = computed(() => route.path.split("/").filter(Boolean));
|
|
159
|
+
const databaseParam = computed(() => {
|
|
160
|
+
const param = route.params.database;
|
|
161
|
+
return typeof param === "string" ? param : undefined;
|
|
162
|
+
});
|
|
163
|
+
const hideDatabaseInBreadcrumb = computed(
|
|
164
|
+
() =>
|
|
165
|
+
!!databaseParam.value &&
|
|
166
|
+
routeSegments.value[0] === databaseParam.value &&
|
|
167
|
+
routeSegments.value[1] === "admin",
|
|
168
|
+
);
|
|
169
|
+
const breadcrumbArray = computed(() =>
|
|
170
|
+
hideDatabaseInBreadcrumb.value
|
|
171
|
+
? routeSegments.value.slice(1)
|
|
172
|
+
: routeSegments.value,
|
|
173
|
+
);
|
|
174
|
+
const table = useState<Table>("table");
|
|
175
|
+
const currentItem = useState<Item>("currentItem");
|
|
176
|
+
const currentDashboard = useState<Dashboard | null>("currentDashboard");
|
|
177
|
+
const itemLabel = computed(() => renderLabel(table.value, currentItem.value));
|
|
178
|
+
|
|
179
|
+
function breadCrumbItemLabel(index: number) {
|
|
180
|
+
const childRoute = breadcrumbArray.value[index];
|
|
181
|
+
if (isValidID(childRoute)) {
|
|
182
|
+
if (currentDashboard.value?.name && route.path.includes("/dashboards/"))
|
|
183
|
+
return currentDashboard.value.name;
|
|
184
|
+
if (currentItem.value) return itemLabel.value;
|
|
185
|
+
}
|
|
186
|
+
if (childRoute === "admin") return t("adminPanel");
|
|
187
|
+
return t(childRoute);
|
|
188
|
+
}
|
|
189
|
+
const userDropdownOptions = computed(() => [
|
|
190
|
+
{
|
|
191
|
+
label: t("apiDocumentation"),
|
|
192
|
+
key: "api",
|
|
193
|
+
icon: () => h(NIcon, () => h(Icon, { name: "tabler:book" })),
|
|
194
|
+
show: !!user.value?.id,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
label: t("settings"),
|
|
198
|
+
key: "settings",
|
|
199
|
+
icon: () => h(NIcon, () => h(Icon, { name: "tabler:settings" })),
|
|
200
|
+
show: user.value?.role === config.public.idOne,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
label: () => h(NFlex,{ justify: "start", align: "center" }, () => [
|
|
204
|
+
t("billing"),
|
|
205
|
+
h(NIcon, { color: ThemeConfig.value.primaryColor}, () => h(Icon, { name: "tabler:external-link" }))
|
|
206
|
+
]),
|
|
207
|
+
key: "billing",
|
|
208
|
+
icon: () => h(NIcon, () => h(Icon, { name: "tabler:credit-card" })),
|
|
209
|
+
show:
|
|
210
|
+
!!user.value?.id &&
|
|
211
|
+
(database.value?.slug === "inicontent" ||
|
|
212
|
+
user.value?.role === config.public.idOne),
|
|
213
|
+
disabled: route.path.includes("/admin/billing"),
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
label: t("toggleTheme"),
|
|
217
|
+
key: "theme",
|
|
218
|
+
icon: () =>
|
|
219
|
+
h(NIcon, () =>
|
|
220
|
+
h(Icon, {
|
|
221
|
+
name: Theme.value === "light" ? "tabler:moon" : "tabler:sun",
|
|
222
|
+
}),
|
|
223
|
+
),
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
label: t("profile"),
|
|
227
|
+
key: "edit",
|
|
228
|
+
icon: () => h(NIcon, () => h(Icon, { name: "tabler:pencil" })),
|
|
229
|
+
show:
|
|
230
|
+
!!user.value?.id &&
|
|
231
|
+
database.value?.tables
|
|
232
|
+
?.find(({ slug }) => slug === "users")
|
|
233
|
+
?.allowedMethods?.includes("u"),
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
label: t("logout"),
|
|
237
|
+
key: "logout",
|
|
238
|
+
icon: () => h(NIcon, () => h(Icon, { name: "tabler:logout" })),
|
|
239
|
+
show: !!user.value?.id,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
label: t("auth"),
|
|
243
|
+
key: "auth",
|
|
244
|
+
icon: () => h(NIcon, () => h(Icon, { name: "tabler:login" })),
|
|
245
|
+
show: !user.value?.id,
|
|
246
|
+
disabled: (route.name as string | undefined)?.endsWith("-auth"),
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
label: `${t("clearCache")} (${humanFileSize(cacheStats.value.size)})`,
|
|
250
|
+
key: "clearCache",
|
|
251
|
+
icon: () => h(NIcon, () => h(Icon, { name: "tabler:wash-machine" })),
|
|
252
|
+
show: !!user.value?.id && cacheStats.value.size > 0,
|
|
253
|
+
},
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
const sessionID = useScopedCookie<string>("sid", database.value?.slug);
|
|
257
|
+
const { platformUrl } = usePlatformRedirect();
|
|
258
|
+
|
|
259
|
+
async function onSelectUserDropdown(v: string) {
|
|
260
|
+
switch (v) {
|
|
261
|
+
case "api":
|
|
262
|
+
navigateTo(
|
|
263
|
+
`${route.params.database ? `/${route.params.database}` : ""}/admin/api`,
|
|
264
|
+
);
|
|
265
|
+
break;
|
|
266
|
+
case "edit":
|
|
267
|
+
navigateTo(
|
|
268
|
+
`${route.params.database ? `/${route.params.database}` : ""}/admin/tables/users/${
|
|
269
|
+
(user.value as User).id
|
|
270
|
+
}/edit`,
|
|
271
|
+
);
|
|
272
|
+
break;
|
|
273
|
+
case "billing":
|
|
274
|
+
navigateTo(platformUrl("billing"), { external: true });
|
|
275
|
+
break;
|
|
276
|
+
case "settings":
|
|
277
|
+
navigateTo(
|
|
278
|
+
`${route.params.database ? `/${route.params.database}` : ""}/admin/settings`,
|
|
279
|
+
);
|
|
280
|
+
break;
|
|
281
|
+
case "theme":
|
|
282
|
+
Theme.value = Theme.value === "dark" ? "light" : "dark";
|
|
283
|
+
break;
|
|
284
|
+
case "logout":
|
|
285
|
+
await $fetch(
|
|
286
|
+
`${config.public.apiBase}${
|
|
287
|
+
database.value?.slug ?? "inicontent"
|
|
288
|
+
}/auth/signout`,
|
|
289
|
+
{
|
|
290
|
+
credentials: "include",
|
|
291
|
+
params: {
|
|
292
|
+
locale: Language.value,
|
|
293
|
+
[`${database.value.slug}_sid`]: sessionID.value,
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
);
|
|
297
|
+
redirectTo.value = undefined;
|
|
298
|
+
user.value = undefined;
|
|
299
|
+
await navigateTo(
|
|
300
|
+
`${route.params.database ? `/${route.params.database}` : ""}/auth`,
|
|
301
|
+
);
|
|
302
|
+
break;
|
|
303
|
+
case "clearCache":
|
|
304
|
+
await clearAllCache();
|
|
305
|
+
cacheStats.value = await getCacheStats();
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const cacheStats = ref<{ total?: number; expired?: number; size: number }>({
|
|
310
|
+
size: 0,
|
|
311
|
+
});
|
|
312
|
+
onMounted(async () => {
|
|
313
|
+
cacheStats.value = await getCacheStats();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const languagesDropdownOptions = computed(() => {
|
|
317
|
+
const primary = database.value?.primaryLanguage;
|
|
318
|
+
const secondary = database.value?.secondaryLanguages ?? [];
|
|
319
|
+
const languages = [primary, ...secondary].filter(
|
|
320
|
+
(language): language is LanguagesType => !!language,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
return [...new Set(languages)].map((language) => ({
|
|
324
|
+
label: t(`languages.${language}`),
|
|
325
|
+
key: language,
|
|
326
|
+
}));
|
|
327
|
+
});
|
|
328
|
+
</script>
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<NModal
|
|
3
|
+
v-model:show="isOpen"
|
|
4
|
+
preset="card"
|
|
5
|
+
:title="t('resolveConflicts')"
|
|
6
|
+
:style="{ width: 'min(720px, calc(100vw - 32px))' }"
|
|
7
|
+
:mask-closable="false"
|
|
8
|
+
>
|
|
9
|
+
<template #header-extra>
|
|
10
|
+
<NButton
|
|
11
|
+
v-if="conflicts.length"
|
|
12
|
+
size="small"
|
|
13
|
+
type="error"
|
|
14
|
+
quaternary
|
|
15
|
+
@click="onDiscardAll"
|
|
16
|
+
>
|
|
17
|
+
{{ t('discardAll') }}
|
|
18
|
+
</NButton>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<!-- Explain why this popped up: an offline edit collided with the server -->
|
|
22
|
+
<NAlert
|
|
23
|
+
v-if="conflicts.length"
|
|
24
|
+
type="warning"
|
|
25
|
+
:show-icon="true"
|
|
26
|
+
style="margin-bottom: 16px"
|
|
27
|
+
>
|
|
28
|
+
{{ t('conflictModalIntro') }}
|
|
29
|
+
</NAlert>
|
|
30
|
+
|
|
31
|
+
<NSpin :show="isSyncing">
|
|
32
|
+
<NFlex v-if="conflicts.length === 0 && !isSyncing" vertical align="center" justify="center"
|
|
33
|
+
style="padding: 60px 0; gap: 12px">
|
|
34
|
+
<NIcon size="48" color="#888">
|
|
35
|
+
<Icon name="tabler:circle-check" />
|
|
36
|
+
</NIcon>
|
|
37
|
+
<NText depth="3">{{ t('noConflicts') }}</NText>
|
|
38
|
+
</NFlex>
|
|
39
|
+
|
|
40
|
+
<NFlex vertical :size="16">
|
|
41
|
+
<NCard
|
|
42
|
+
v-for="conflict in conflicts"
|
|
43
|
+
:key="conflict.id"
|
|
44
|
+
:title="`${conflict.method} · ${conflict.table || conflict.database}`"
|
|
45
|
+
size="small"
|
|
46
|
+
bordered
|
|
47
|
+
>
|
|
48
|
+
<template #header-extra>
|
|
49
|
+
<NTag size="small" type="warning" :bordered="false">
|
|
50
|
+
{{ conflict.method }}
|
|
51
|
+
</NTag>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<NDescriptions :column="1" size="small" label-placement="top">
|
|
55
|
+
<NDescriptionsItem :label="t('localVersion')">
|
|
56
|
+
<pre class="json">{{ pretty(conflict.conflictData?.local) }}</pre>
|
|
57
|
+
</NDescriptionsItem>
|
|
58
|
+
<NDescriptionsItem :label="t('serverVersion')">
|
|
59
|
+
<pre class="json">{{ pretty(conflict.conflictData?.server) }}</pre>
|
|
60
|
+
</NDescriptionsItem>
|
|
61
|
+
</NDescriptions>
|
|
62
|
+
|
|
63
|
+
<template #footer>
|
|
64
|
+
<NFlex justify="end" :size="8">
|
|
65
|
+
<NButton
|
|
66
|
+
size="small"
|
|
67
|
+
type="error"
|
|
68
|
+
@click="keepServer(conflict.id)"
|
|
69
|
+
>
|
|
70
|
+
<template #icon>
|
|
71
|
+
<NIcon><Icon name="tabler:server" /></NIcon>
|
|
72
|
+
</template>
|
|
73
|
+
{{ t('keepServer') }}
|
|
74
|
+
</NButton>
|
|
75
|
+
<NButton
|
|
76
|
+
size="small"
|
|
77
|
+
type="primary"
|
|
78
|
+
@click="keepLocal(conflict.id)"
|
|
79
|
+
>
|
|
80
|
+
<template #icon>
|
|
81
|
+
<NIcon><Icon name="tabler:device-mobile" /></NIcon>
|
|
82
|
+
</template>
|
|
83
|
+
{{ t('keepLocal') }}
|
|
84
|
+
</NButton>
|
|
85
|
+
</NFlex>
|
|
86
|
+
</template>
|
|
87
|
+
</NCard>
|
|
88
|
+
</NFlex>
|
|
89
|
+
</NSpin>
|
|
90
|
+
</NModal>
|
|
91
|
+
</template>
|
|
92
|
+
|
|
93
|
+
<script setup lang="ts">
|
|
94
|
+
import {
|
|
95
|
+
Icon,
|
|
96
|
+
NAlert,
|
|
97
|
+
NButton,
|
|
98
|
+
NCard,
|
|
99
|
+
NDescriptions,
|
|
100
|
+
NDescriptionsItem,
|
|
101
|
+
NFlex,
|
|
102
|
+
NIcon,
|
|
103
|
+
NModal,
|
|
104
|
+
NSpin,
|
|
105
|
+
NTag,
|
|
106
|
+
NText,
|
|
107
|
+
} from "#components";
|
|
108
|
+
import {
|
|
109
|
+
discardAllConflicts,
|
|
110
|
+
resolveConflictKeepLocal,
|
|
111
|
+
resolveConflictKeepServer,
|
|
112
|
+
useOfflineSync,
|
|
113
|
+
} from "~/composables/useOfflineSync";
|
|
114
|
+
|
|
115
|
+
// Shared bus: SyncStatus's "resolve conflicts" action opens the *same* modal.
|
|
116
|
+
// Keeping this a useState (not component-local) means the modal can live
|
|
117
|
+
// globally in the layout and still be reachable from anywhere.
|
|
118
|
+
const isOpen = useState<boolean>("offline_conflictModalOpen", () => false);
|
|
119
|
+
|
|
120
|
+
// Destructure at the top level so the template compiler auto-unwraps these
|
|
121
|
+
// refs. Accessing them as `sync.conflicts` (properties of the plain object
|
|
122
|
+
// returned by useOfflineSync) would NOT unwrap: `v-for="conflict in
|
|
123
|
+
// sync.conflicts"` would iterate the raw Ref and crash rendering.
|
|
124
|
+
const { conflicts, isSyncing, refreshCounts } = useOfflineSync();
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Auto-open the modal whenever a NEW conflict batch is detected — i.e. a
|
|
128
|
+
* queued offline mutation collided with the server during a sync (typically
|
|
129
|
+
* right after the device reconnects). Re-armed when the conflict list empties
|
|
130
|
+
* or when the user manually closes the modal, so a later conflict batch pops
|
|
131
|
+
* it again instead of silently parking behind the header badge.
|
|
132
|
+
*/
|
|
133
|
+
const armed = ref(true);
|
|
134
|
+
|
|
135
|
+
function arm() {
|
|
136
|
+
armed.value = true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
watch(
|
|
140
|
+
() => conflicts.value.length,
|
|
141
|
+
(count) => {
|
|
142
|
+
if (count === 0) {
|
|
143
|
+
arm();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (armed.value) {
|
|
147
|
+
isOpen.value = true;
|
|
148
|
+
armed.value = false;
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
watch(isOpen, (open) => {
|
|
154
|
+
if (!open) arm();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
function pretty(value: any): string {
|
|
158
|
+
if (value === undefined || value === null) return "—";
|
|
159
|
+
if (typeof value === "string") {
|
|
160
|
+
try {
|
|
161
|
+
return JSON.stringify(JSON.parse(value), null, 2);
|
|
162
|
+
} catch {
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return JSON.stringify(value, null, 2);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function keepLocal(id: string) {
|
|
170
|
+
await resolveConflictKeepLocal(id);
|
|
171
|
+
await refreshCounts();
|
|
172
|
+
window.$message?.success(t("changeQueuedForSync"));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function keepServer(id: string) {
|
|
176
|
+
await resolveConflictKeepServer(id);
|
|
177
|
+
await refreshCounts();
|
|
178
|
+
window.$message?.info(t("localChangeDiscarded"));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function onDiscardAll() {
|
|
182
|
+
await discardAllConflicts();
|
|
183
|
+
await refreshCounts();
|
|
184
|
+
window.$message?.info(t("allLocalChangesDiscarded"));
|
|
185
|
+
}
|
|
186
|
+
</script>
|
|
187
|
+
|
|
188
|
+
<style scoped>
|
|
189
|
+
.json {
|
|
190
|
+
background: rgba(127, 127, 127, 0.08);
|
|
191
|
+
border-radius: 6px;
|
|
192
|
+
padding: 8px;
|
|
193
|
+
font-size: 12px;
|
|
194
|
+
white-space: pre-wrap;
|
|
195
|
+
word-break: break-word;
|
|
196
|
+
max-height: 240px;
|
|
197
|
+
overflow: auto;
|
|
198
|
+
margin: 0;
|
|
199
|
+
}
|
|
200
|
+
</style>
|