@kyro-cms/admin 0.1.7 → 0.1.8
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/package.json +5 -3
- package/src/components/Admin.tsx +1 -1
- package/src/components/AutoForm.tsx +966 -337
- package/src/components/CreateView.tsx +1 -1
- package/src/components/DetailView.tsx +1 -1
- package/src/components/EnhancedListView.tsx +156 -52
- package/src/components/ListView.tsx +1 -1
- package/src/components/Modal.tsx +65 -8
- package/src/components/Sidebar.astro +2 -2
- package/src/components/ThemeProvider.tsx +8 -2
- package/src/components/blocks/AccordionBlock.tsx +20 -52
- package/src/components/blocks/ArrayBlock.tsx +40 -31
- package/src/components/blocks/BlockEditModal.tsx +170 -581
- package/src/components/blocks/ButtonBlock.tsx +27 -128
- package/src/components/blocks/CodeBlock.tsx +88 -40
- package/src/components/blocks/ColumnsBlock.tsx +27 -85
- package/src/components/blocks/FileBlock.tsx +38 -39
- package/src/components/blocks/HeadingBlock.tsx +9 -31
- package/src/components/blocks/HeroBlock.tsx +42 -100
- package/src/components/blocks/ImageBlock.tsx +6 -7
- package/src/components/blocks/LinkBlock.tsx +27 -33
- package/src/components/blocks/ListBlock.tsx +47 -26
- package/src/components/blocks/RelationshipBlock.tsx +26 -233
- package/src/components/blocks/RichTextBlock.tsx +66 -0
- package/src/components/blocks/VStackBlock.tsx +23 -37
- package/src/components/blocks/VideoBlock.tsx +52 -32
- package/src/components/fields/AccordionField.tsx +213 -0
- package/src/components/fields/ArrayField.tsx +241 -0
- package/src/components/fields/BlocksField.tsx +5 -5
- package/src/components/fields/ButtonField.tsx +53 -0
- package/src/components/fields/CheckboxField.tsx +7 -3
- package/src/components/fields/ChildrenField.tsx +48 -0
- package/src/components/fields/CodeField.tsx +154 -94
- package/src/components/fields/ColumnsField.tsx +137 -0
- package/src/components/fields/DateField.tsx +9 -24
- package/src/components/fields/EditorClient.tsx +426 -160
- package/src/components/fields/HeadingField.tsx +31 -0
- package/src/components/fields/HeroField.tsx +101 -0
- package/src/components/fields/JSONField.tsx +7 -27
- package/src/components/fields/LinkField.tsx +81 -0
- package/src/components/fields/ListField.tsx +74 -0
- package/src/components/fields/MarkdownField.tsx +4 -26
- package/src/components/fields/NumberField.tsx +9 -27
- package/src/components/fields/PortableTextField.tsx +61 -49
- package/src/components/fields/RelationshipBlockField.tsx +233 -0
- package/src/components/fields/RelationshipField.tsx +59 -13
- package/src/components/fields/SelectField.tsx +6 -4
- package/src/components/fields/TextField.tsx +9 -24
- package/src/components/fields/UploadField.tsx +613 -0
- package/src/components/fields/VideoField.tsx +73 -0
- package/src/components/fields/extensions/blockComponents.tsx +11 -1
- package/src/components/fields/extensions/blocksStore.ts +1 -1
- package/src/components/fields/index.ts +12 -1
- package/src/components/layout/Layout.tsx +1 -1
- package/src/lib/api.ts +163 -0
- package/src/lib/config.ts +1 -1
- package/src/lib/dataStore.ts +87 -30
- package/src/lib/date-utils.ts +69 -0
- package/src/lib/db/version-adapter.ts +248 -0
- package/src/lib/i18n.tsx +353 -0
- package/src/lib/slugify.ts +15 -0
- package/src/lib/validation.ts +250 -0
- package/src/pages/api/[collection]/[id]/publish.ts +12 -4
- package/src/pages/api/[collection]/[id]/versions.ts +39 -9
- package/src/pages/api/[collection]/[id].ts +13 -1
- package/src/pages/api/[collection]/index.ts +5 -6
- package/src/styles/main.css +12 -2
- package/src/components/blocks/BlockEditModal.MARKER +0 -12
- package/src/components/fields/FileField.tsx +0 -390
- package/src/components/fields/HybridContentField.tsx +0 -109
- package/src/components/fields/ImageField.tsx +0 -429
|
@@ -12,6 +12,7 @@ import { FileBlock } from "../../blocks/FileBlock";
|
|
|
12
12
|
import { VStackBlock } from "../../blocks/VStackBlock";
|
|
13
13
|
import { ButtonBlock } from "../../blocks/ButtonBlock";
|
|
14
14
|
import { AccordionBlock } from "../../blocks/AccordionBlock";
|
|
15
|
+
import { RichTextBlock } from "../../blocks/RichTextBlock";
|
|
15
16
|
|
|
16
17
|
import { HeroBlock } from "../../blocks/HeroBlock";
|
|
17
18
|
import { ArrayBlock } from "../../blocks/ArrayBlock";
|
|
@@ -51,6 +52,7 @@ export const BLOCK_COMPONENTS: Record<string, React.ComponentType<any>> = {
|
|
|
51
52
|
vstack: VStackBlock,
|
|
52
53
|
button: ButtonBlock,
|
|
53
54
|
accordion: AccordionBlock,
|
|
55
|
+
richtext: RichTextBlock,
|
|
54
56
|
|
|
55
57
|
hero: HeroBlock,
|
|
56
58
|
array: ArrayBlock,
|
|
@@ -72,6 +74,7 @@ export const blockIcons: Record<string, React.ReactNode> = {
|
|
|
72
74
|
vstack: <ArrowDown className="w-4 h-4" />,
|
|
73
75
|
button: <MousePointerClick className="w-4 h-4" />,
|
|
74
76
|
accordion: <ChevronDown className="w-4 h-4" />,
|
|
77
|
+
richtext: <AlignLeft className="w-4 h-4" />,
|
|
75
78
|
|
|
76
79
|
hero: <Star className="w-4 h-4" />,
|
|
77
80
|
array: <ListOrdered className="w-4 h-4" />,
|
|
@@ -107,6 +110,7 @@ export function getBlockLabel(type: string): string {
|
|
|
107
110
|
vstack: "VStack",
|
|
108
111
|
columns: "Columns",
|
|
109
112
|
relationship: "Relationship",
|
|
113
|
+
richtext: "Rich Text",
|
|
110
114
|
};
|
|
111
115
|
return labelMap[type] || type;
|
|
112
116
|
}
|
|
@@ -143,7 +147,7 @@ export const blockCategories = [
|
|
|
143
147
|
type: "heading",
|
|
144
148
|
label: "Heading",
|
|
145
149
|
icon: "heading",
|
|
146
|
-
description: "
|
|
150
|
+
description: "Heading text",
|
|
147
151
|
},
|
|
148
152
|
{
|
|
149
153
|
type: "paragraph",
|
|
@@ -151,6 +155,12 @@ export const blockCategories = [
|
|
|
151
155
|
icon: "paragraph",
|
|
152
156
|
description: "Plain text content",
|
|
153
157
|
},
|
|
158
|
+
{
|
|
159
|
+
type: "richtext",
|
|
160
|
+
label: "Rich Text",
|
|
161
|
+
icon: "richtext",
|
|
162
|
+
description: "Formatted text with links & styles",
|
|
163
|
+
},
|
|
154
164
|
{
|
|
155
165
|
type: "list",
|
|
156
166
|
label: "List",
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { default as PortableTextField } from "./PortableTextField";
|
|
2
2
|
export { PortableTextRenderer } from "./PortableTextRenderer";
|
|
3
|
-
export { HybridContentField } from "./HybridContentField";
|
|
4
3
|
export { CodeField } from "./CodeField";
|
|
5
4
|
export { JSONField } from "./JSONField";
|
|
6
5
|
export { MarkdownField } from "./MarkdownField";
|
|
@@ -11,3 +10,15 @@ export { default as DateField } from "./DateField";
|
|
|
11
10
|
export { default as SelectField } from "./SelectField";
|
|
12
11
|
export { default as RelationshipField } from "./RelationshipField";
|
|
13
12
|
export { BlocksField } from "./BlocksField";
|
|
13
|
+
export { AccordionField } from "./AccordionField";
|
|
14
|
+
export { ButtonField } from "./ButtonField";
|
|
15
|
+
export { UploadField } from "./UploadField";
|
|
16
|
+
export { LinkField } from "./LinkField";
|
|
17
|
+
export { HeadingField } from "./HeadingField";
|
|
18
|
+
export { VideoField } from "./VideoField";
|
|
19
|
+
export { ListField } from "./ListField";
|
|
20
|
+
export { HeroField } from "./HeroField";
|
|
21
|
+
export { ArrayField } from "./ArrayField";
|
|
22
|
+
export { ChildrenField } from "./ChildrenField";
|
|
23
|
+
export { ColumnsField } from "./ColumnsField";
|
|
24
|
+
export { RelationshipBlockField } from "./RelationshipBlockField";
|
package/src/lib/api.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
export interface ApiResponse<T = any> {
|
|
2
|
+
docs?: T[];
|
|
3
|
+
doc?: T;
|
|
4
|
+
totalDocs?: number;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function apiGet<T = any>(
|
|
9
|
+
url: string,
|
|
10
|
+
options?: RequestInit,
|
|
11
|
+
): Promise<T> {
|
|
12
|
+
const response = await fetch(url, {
|
|
13
|
+
...options,
|
|
14
|
+
credentials: "include",
|
|
15
|
+
});
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
throw new Error(`API Error: ${response.status}`);
|
|
18
|
+
}
|
|
19
|
+
return response.json();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function apiPost<T = any>(
|
|
23
|
+
url: string,
|
|
24
|
+
body?: any,
|
|
25
|
+
options?: RequestInit,
|
|
26
|
+
): Promise<T> {
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
...options?.headers,
|
|
32
|
+
},
|
|
33
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
34
|
+
credentials: "include",
|
|
35
|
+
...options,
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error(`API Error: ${response.status}`);
|
|
39
|
+
}
|
|
40
|
+
return response.json();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function apiPatch<T = any>(
|
|
44
|
+
url: string,
|
|
45
|
+
body?: any,
|
|
46
|
+
options?: RequestInit,
|
|
47
|
+
): Promise<T> {
|
|
48
|
+
const response = await fetch(url, {
|
|
49
|
+
method: "PATCH",
|
|
50
|
+
headers: {
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
...options?.headers,
|
|
53
|
+
},
|
|
54
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
55
|
+
credentials: "include",
|
|
56
|
+
...options,
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
throw new Error(`API Error: ${response.status}`);
|
|
60
|
+
}
|
|
61
|
+
return response.json();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function apiPatchNoThrow<T = any>(
|
|
65
|
+
url: string,
|
|
66
|
+
body?: any,
|
|
67
|
+
): Promise<{ ok: boolean; data?: T; error?: string }> {
|
|
68
|
+
const response = await fetch(url, {
|
|
69
|
+
method: "PATCH",
|
|
70
|
+
headers: { "Content-Type": "application/json" },
|
|
71
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
72
|
+
credentials: "include",
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
return { ok: false, error: `Error: ${response.status}` };
|
|
76
|
+
}
|
|
77
|
+
const data = await response.json();
|
|
78
|
+
return { ok: true, data };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function apiDelete<T = any>(
|
|
82
|
+
url: string,
|
|
83
|
+
options?: RequestInit,
|
|
84
|
+
): Promise<T> {
|
|
85
|
+
const response = await fetch(url, {
|
|
86
|
+
method: "DELETE",
|
|
87
|
+
credentials: "include",
|
|
88
|
+
...options,
|
|
89
|
+
});
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(`API Error: ${response.status}`);
|
|
92
|
+
}
|
|
93
|
+
return response.json();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function buildQueryString(params: Record<string, any>): string {
|
|
97
|
+
const urlParams = new URLSearchParams();
|
|
98
|
+
for (const [key, value] of Object.entries(params)) {
|
|
99
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
100
|
+
urlParams.set(key, String(value));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return urlParams.toString();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function withCacheBust(url: string): string {
|
|
107
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
108
|
+
return `${url}${separator}t=${Date.now()}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function buildSearchQuery(
|
|
112
|
+
search: string,
|
|
113
|
+
fields: string[],
|
|
114
|
+
limit: number = 50,
|
|
115
|
+
): string {
|
|
116
|
+
if (!search || fields.length === 0) {
|
|
117
|
+
return `limit=${limit}`;
|
|
118
|
+
}
|
|
119
|
+
const searchQuery = fields
|
|
120
|
+
.map((f) => `where[${f}][contains]=${encodeURIComponent(search)}`)
|
|
121
|
+
.join("&");
|
|
122
|
+
return `${searchQuery}&limit=${limit}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function buildCollectionUrl(
|
|
126
|
+
collection: string,
|
|
127
|
+
params?: Record<string, any>,
|
|
128
|
+
): string {
|
|
129
|
+
let url = `/api/${collection}`;
|
|
130
|
+
if (params) {
|
|
131
|
+
const query = buildQueryString(params);
|
|
132
|
+
if (query) url += `?${query}`;
|
|
133
|
+
}
|
|
134
|
+
return withCacheBust(url);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function buildDocumentUrl(
|
|
138
|
+
collection: string,
|
|
139
|
+
id: string,
|
|
140
|
+
params?: Record<string, any>,
|
|
141
|
+
): string {
|
|
142
|
+
let url = `/api/${collection}/${id}`;
|
|
143
|
+
if (params) {
|
|
144
|
+
const query = buildQueryString(params);
|
|
145
|
+
if (query) url += `?${query}`;
|
|
146
|
+
}
|
|
147
|
+
return url;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function apiUpload<T = any>(
|
|
151
|
+
url: string,
|
|
152
|
+
body: FormData,
|
|
153
|
+
): Promise<T> {
|
|
154
|
+
const response = await fetch(url, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
body,
|
|
157
|
+
credentials: "include",
|
|
158
|
+
});
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
throw new Error(`Upload Error: ${response.status}`);
|
|
161
|
+
}
|
|
162
|
+
return response.json();
|
|
163
|
+
}
|
package/src/lib/config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CollectionConfig, GlobalConfig } from "@kyro-cms/core";
|
|
1
|
+
import type { CollectionConfig, GlobalConfig } from "@kyro-cms/core/client";
|
|
2
2
|
import { blogCollections } from "../../../src/templates/blog";
|
|
3
3
|
import { ecommerceCollections } from "../../../src/templates/ecommerce";
|
|
4
4
|
import { minimalCollections } from "../../../src/templates/minimal";
|
package/src/lib/dataStore.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import type { CollectionConfig } from "@kyro-cms/core";
|
|
1
|
+
import type { CollectionConfig } from "@kyro-cms/core/client";
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
3
|
import {
|
|
4
4
|
initializeDatabase,
|
|
5
5
|
getDatabaseAdapter,
|
|
6
6
|
isDatabaseInitialized,
|
|
7
7
|
} from "./db";
|
|
8
|
+
import type { Version, VersionDiff, DraftPublishConfig } from "@kyro-cms/core";
|
|
9
|
+
import { VersionManager, createVersionManager } from "@kyro-cms/core";
|
|
10
|
+
import { DataStoreVersionAdapter } from "./db/version-adapter.js";
|
|
8
11
|
|
|
9
12
|
class DataStoreWrapper {
|
|
13
|
+
private db: ReturnType<typeof getDatabaseAdapter> | null = null;
|
|
14
|
+
private versionManager: VersionManager | null = null;
|
|
15
|
+
|
|
10
16
|
private async getStore() {
|
|
11
17
|
if (!isDatabaseInitialized()) {
|
|
12
18
|
await initializeDatabase();
|
|
@@ -14,10 +20,19 @@ class DataStoreWrapper {
|
|
|
14
20
|
return getDatabaseAdapter();
|
|
15
21
|
}
|
|
16
22
|
|
|
17
|
-
private
|
|
23
|
+
private async getVersionManager(): Promise<VersionManager> {
|
|
24
|
+
if (this.versionManager) return this.versionManager;
|
|
25
|
+
const db = await this.getStore();
|
|
26
|
+
const adapter = new DataStoreVersionAdapter(db);
|
|
27
|
+
this.versionManager = createVersionManager(adapter, {
|
|
28
|
+
versioningEnabled: true,
|
|
29
|
+
maxVersionsPerDocument: 50,
|
|
30
|
+
} as DraftPublishConfig);
|
|
31
|
+
return this.versionManager;
|
|
32
|
+
}
|
|
18
33
|
|
|
19
34
|
initialize(collections: Record<string, CollectionConfig>) {
|
|
20
|
-
initializeDatabase(collections);
|
|
35
|
+
initializeDatabase(collections);
|
|
21
36
|
}
|
|
22
37
|
|
|
23
38
|
private async getTimestamp(): Promise<string> {
|
|
@@ -49,13 +64,13 @@ class DataStoreWrapper {
|
|
|
49
64
|
async create<T = any>(slug: string, data: Partial<T>): Promise<T> {
|
|
50
65
|
const store = await this.getStore();
|
|
51
66
|
const now = await this.getTimestamp();
|
|
52
|
-
const id = data?.id || this.generateId();
|
|
67
|
+
const id = (data as any)?.id || this.generateId();
|
|
53
68
|
const newDoc = {
|
|
54
69
|
...data,
|
|
55
70
|
id,
|
|
56
71
|
createdAt: now,
|
|
57
72
|
updatedAt: now,
|
|
58
|
-
};
|
|
73
|
+
} as T;
|
|
59
74
|
return store.create(slug, newDoc);
|
|
60
75
|
}
|
|
61
76
|
|
|
@@ -63,6 +78,10 @@ class DataStoreWrapper {
|
|
|
63
78
|
slug: string,
|
|
64
79
|
id: string,
|
|
65
80
|
data: Partial<T>,
|
|
81
|
+
options?: {
|
|
82
|
+
versionStatus?: "draft" | "published";
|
|
83
|
+
changeDescription?: string;
|
|
84
|
+
},
|
|
66
85
|
): Promise<T | null> {
|
|
67
86
|
const store = await this.getStore();
|
|
68
87
|
const existing = await store.findById(slug, id);
|
|
@@ -77,48 +96,86 @@ class DataStoreWrapper {
|
|
|
77
96
|
};
|
|
78
97
|
|
|
79
98
|
// Save version history before updating
|
|
80
|
-
|
|
99
|
+
const versionStatus = options?.versionStatus || "draft";
|
|
100
|
+
const changeDescription = options?.changeDescription;
|
|
101
|
+
|
|
102
|
+
await this.createVersion(slug, id, existing, {
|
|
103
|
+
status: versionStatus,
|
|
104
|
+
changeDescription,
|
|
105
|
+
});
|
|
81
106
|
|
|
82
107
|
return store.update(slug, id, updated);
|
|
83
108
|
}
|
|
84
109
|
|
|
85
110
|
async findVersions(parentCollection: string, parentId: string) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
// but for now we'll fetch and filter in memory if the adapter is basic
|
|
89
|
-
const result = await store.find(this.VERSIONS_COLLECTION, { limit: 100 });
|
|
90
|
-
return result.docs
|
|
91
|
-
.filter((v: any) => v.parentCollection === parentCollection && v.parentId === parentId)
|
|
92
|
-
.sort((a: any, b: any) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async createVersion(parentCollection: string, parentId: string, data: any) {
|
|
96
|
-
const store = await this.getStore();
|
|
97
|
-
const now = await this.getTimestamp();
|
|
98
|
-
const versionDoc = {
|
|
99
|
-
id: this.generateId(),
|
|
100
|
-
parentId,
|
|
111
|
+
const vm = await this.getVersionManager();
|
|
112
|
+
const versions = await vm.getVersionHistory(
|
|
101
113
|
parentCollection,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
114
|
+
parentId,
|
|
115
|
+
100,
|
|
116
|
+
);
|
|
117
|
+
return (versions as Version[])
|
|
118
|
+
.map((v) => ({
|
|
119
|
+
id: v.id,
|
|
120
|
+
version: v.version,
|
|
121
|
+
createdAt: v.createdAt,
|
|
122
|
+
status: v.status,
|
|
123
|
+
createdBy: v.createdBy,
|
|
124
|
+
data: v.data,
|
|
125
|
+
changeDescription: v.changeDescription,
|
|
126
|
+
}))
|
|
127
|
+
.sort((a, b) => b.version - a.version);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async createVersion(
|
|
131
|
+
parentCollection: string,
|
|
132
|
+
parentId: string,
|
|
133
|
+
data: any,
|
|
134
|
+
options?: {
|
|
135
|
+
status?: string;
|
|
136
|
+
createdBy?: string;
|
|
137
|
+
changeDescription?: string;
|
|
138
|
+
},
|
|
139
|
+
) {
|
|
140
|
+
const vm = await this.getVersionManager();
|
|
106
141
|
try {
|
|
107
|
-
return await
|
|
142
|
+
return await vm.createVersion({
|
|
143
|
+
collection: parentCollection,
|
|
144
|
+
documentId: parentId,
|
|
145
|
+
data,
|
|
146
|
+
status: (options?.status || "draft") as "draft" | "published",
|
|
147
|
+
createdBy: options?.createdBy || "system",
|
|
148
|
+
changeDescription: options?.changeDescription,
|
|
149
|
+
});
|
|
108
150
|
} catch (e) {
|
|
109
151
|
console.error("Failed to create version snapshot:", e);
|
|
110
152
|
return null;
|
|
111
153
|
}
|
|
112
154
|
}
|
|
113
155
|
|
|
114
|
-
async restoreVersion(
|
|
156
|
+
async restoreVersion(
|
|
157
|
+
parentCollection: string,
|
|
158
|
+
parentId: string,
|
|
159
|
+
versionId: string,
|
|
160
|
+
) {
|
|
161
|
+
const vm = await this.getVersionManager();
|
|
115
162
|
const store = await this.getStore();
|
|
116
|
-
const version = await store.findById(
|
|
163
|
+
const version = await store.findById("_versions", versionId);
|
|
117
164
|
if (!version || version.parentId !== parentId) return null;
|
|
118
165
|
|
|
119
166
|
return this.update(parentCollection, parentId, version.data);
|
|
120
167
|
}
|
|
121
168
|
|
|
169
|
+
async compareVersions(
|
|
170
|
+
collection: string,
|
|
171
|
+
documentId: string,
|
|
172
|
+
versionA: string | number,
|
|
173
|
+
versionB: string | number,
|
|
174
|
+
): Promise<VersionDiff[]> {
|
|
175
|
+
const vm = await this.getVersionManager();
|
|
176
|
+
return vm.compareTwoVersions(collection, documentId, versionA, versionB);
|
|
177
|
+
}
|
|
178
|
+
|
|
122
179
|
async delete(slug: string, id: string): Promise<boolean> {
|
|
123
180
|
const store = await this.getStore();
|
|
124
181
|
return store.delete(slug, id);
|
|
@@ -129,8 +186,8 @@ class DataStoreWrapper {
|
|
|
129
186
|
filter: Record<string, any>,
|
|
130
187
|
): Promise<any | null> {
|
|
131
188
|
const store = await this.getStore();
|
|
132
|
-
const results = await store.find(slug, { limit: 1
|
|
133
|
-
return results.length > 0 ? results[0] : null;
|
|
189
|
+
const results = await store.find(slug, { limit: 1 });
|
|
190
|
+
return results.docs.length > 0 ? results.docs[0] : null;
|
|
134
191
|
}
|
|
135
192
|
|
|
136
193
|
async findGlobal<T = any>(slug: string): Promise<T> {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export type DateStyle = "full" | "date" | "time" | "short";
|
|
2
|
+
|
|
3
|
+
export function formatDate(
|
|
4
|
+
date: string | Date | null | undefined,
|
|
5
|
+
style: DateStyle = "date",
|
|
6
|
+
): string {
|
|
7
|
+
if (!date) return "—";
|
|
8
|
+
const d = new Date(date);
|
|
9
|
+
if (isNaN(d.getTime())) return "—";
|
|
10
|
+
|
|
11
|
+
switch (style) {
|
|
12
|
+
case "full":
|
|
13
|
+
return d.toLocaleString("en-US", {
|
|
14
|
+
dateStyle: "medium",
|
|
15
|
+
timeStyle: "short",
|
|
16
|
+
});
|
|
17
|
+
case "time":
|
|
18
|
+
return d.toLocaleTimeString("en-US", {
|
|
19
|
+
hour: "numeric",
|
|
20
|
+
minute: "2-digit",
|
|
21
|
+
});
|
|
22
|
+
case "short":
|
|
23
|
+
return d.toLocaleDateString("en-US", {
|
|
24
|
+
month: "short",
|
|
25
|
+
day: "numeric",
|
|
26
|
+
});
|
|
27
|
+
case "date":
|
|
28
|
+
default:
|
|
29
|
+
return d.toLocaleDateString("en-US", {
|
|
30
|
+
month: "short",
|
|
31
|
+
day: "numeric",
|
|
32
|
+
year: "numeric",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function formatRelativeTime(
|
|
38
|
+
date: string | Date | null | undefined,
|
|
39
|
+
): string {
|
|
40
|
+
if (!date) return "—";
|
|
41
|
+
const d = new Date(date);
|
|
42
|
+
if (isNaN(d.getTime())) return "—";
|
|
43
|
+
|
|
44
|
+
const now = new Date();
|
|
45
|
+
const diffMs = now.getTime() - d.getTime();
|
|
46
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
47
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
48
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
49
|
+
const diffDay = Math.floor(diffHour / 24);
|
|
50
|
+
|
|
51
|
+
if (diffSec < 60) return "Just now";
|
|
52
|
+
if (diffMin < 60) return `${diffMin}m ago`;
|
|
53
|
+
if (diffHour < 24) return `${diffHour}h ago`;
|
|
54
|
+
if (diffDay < 7) return `${diffDay}d ago`;
|
|
55
|
+
return formatDate(d, "date");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getTimestamp(): string {
|
|
59
|
+
return `?t=${Date.now()}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function generateId(length: number = 16): string {
|
|
63
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
64
|
+
let result = "";
|
|
65
|
+
for (let i = 0; i < length; i++) {
|
|
66
|
+
result += chars[Math.floor(Math.random() * chars.length)];
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|