includio-cms 0.0.60 → 0.0.62
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/dist/admin/client/entry/entry.svelte +32 -10
- package/dist/admin/remote/entry.remote.js +1 -1
- package/dist/core/server/entries/operations/create.d.ts +3 -1
- package/dist/core/server/entries/operations/create.js +16 -9
- package/dist/core/server/entries/operations/get.js +57 -3
- package/dist/core/server/fields/populateEntry.js +1 -1
- package/dist/core/server/fields/resolveUrlFields.d.ts +1 -1
- package/dist/core/server/fields/resolveUrlFields.js +2 -2
- package/dist/db-postgres/index.js +11 -1
- package/package.json +1 -1
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
import { createHybridContext } from './hybrid/hybrid-context.svelte.js';
|
|
30
30
|
import HybridLayout from './hybrid/hybrid-layout.svelte';
|
|
31
31
|
import { onMount } from 'svelte';
|
|
32
|
+
import { get } from 'svelte/store';
|
|
32
33
|
|
|
33
34
|
const contentLanguage = getContentLanguage();
|
|
34
35
|
const remotes = getRemotes();
|
|
@@ -130,10 +131,8 @@
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
async function performAutosave() {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const currentData = JSON.stringify(validatedForm.data);
|
|
134
|
+
const currentFormData = get(form.form);
|
|
135
|
+
const currentData = JSON.stringify(currentFormData);
|
|
137
136
|
if (currentData === lastSavedData) {
|
|
138
137
|
saveStatus = 'saved';
|
|
139
138
|
return;
|
|
@@ -143,7 +142,7 @@
|
|
|
143
142
|
try {
|
|
144
143
|
await remotes.updateEntryVersionCommand({
|
|
145
144
|
entryId: entry.id,
|
|
146
|
-
data:
|
|
145
|
+
data: currentFormData,
|
|
147
146
|
type: 'draft'
|
|
148
147
|
});
|
|
149
148
|
lastSavedData = currentData;
|
|
@@ -185,6 +184,31 @@
|
|
|
185
184
|
autosaveTimer = null;
|
|
186
185
|
}
|
|
187
186
|
|
|
187
|
+
if (type === 'draft') {
|
|
188
|
+
// Drafts save without validation
|
|
189
|
+
const currentFormData = get(form.form);
|
|
190
|
+
saveStatus = 'saving';
|
|
191
|
+
try {
|
|
192
|
+
await remotes.updateEntryVersionCommand({
|
|
193
|
+
entryId: entry.id,
|
|
194
|
+
data: currentFormData,
|
|
195
|
+
type: 'draft'
|
|
196
|
+
});
|
|
197
|
+
lastSavedData = JSON.stringify(currentFormData);
|
|
198
|
+
saveStatus = 'saved';
|
|
199
|
+
toast.success(lang[interfaceLanguage.current].saveDraftToast);
|
|
200
|
+
previewToggle = !previewToggle;
|
|
201
|
+
setTimeout(() => {
|
|
202
|
+
if (saveStatus === 'saved') saveStatus = 'idle';
|
|
203
|
+
}, 3000);
|
|
204
|
+
} catch {
|
|
205
|
+
saveStatus = 'error';
|
|
206
|
+
toast.error(interfaceLanguage.current === 'pl' ? 'Błąd zapisu' : 'Save failed');
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Publish requires validation
|
|
188
212
|
const validatedForm = await form.validateForm();
|
|
189
213
|
|
|
190
214
|
if (validatedForm.valid) {
|
|
@@ -203,9 +227,7 @@
|
|
|
203
227
|
const toastMsg =
|
|
204
228
|
type === 'published-now'
|
|
205
229
|
? lang[interfaceLanguage.current].publishToast
|
|
206
|
-
:
|
|
207
|
-
? lang[interfaceLanguage.current].scheduledToast
|
|
208
|
-
: lang[interfaceLanguage.current].saveToast;
|
|
230
|
+
: lang[interfaceLanguage.current].scheduledToast;
|
|
209
231
|
toast.success(toastMsg);
|
|
210
232
|
previewToggle = !previewToggle;
|
|
211
233
|
|
|
@@ -220,8 +242,8 @@
|
|
|
220
242
|
const errors = flattenErrors(validatedForm.errors, collection.fields);
|
|
221
243
|
toast.error(
|
|
222
244
|
interfaceLanguage.current === 'pl'
|
|
223
|
-
? 'Nie można
|
|
224
|
-
: 'Cannot
|
|
245
|
+
? 'Nie można opublikować'
|
|
246
|
+
: 'Cannot publish',
|
|
225
247
|
{
|
|
226
248
|
description: errors.slice(0, 3).join('\n') + (errors.length > 3 ? '\n...' : ''),
|
|
227
249
|
duration: 5000
|
|
@@ -82,7 +82,7 @@ export const updateEntryVersionCommand = command(updateEntryVersionCommandSchema
|
|
|
82
82
|
let result;
|
|
83
83
|
switch (input.type) {
|
|
84
84
|
case 'draft':
|
|
85
|
-
result = await createEntryVersion(input);
|
|
85
|
+
result = await createEntryVersion(input, { skipValidation: true });
|
|
86
86
|
break;
|
|
87
87
|
case 'published-now':
|
|
88
88
|
result = await createEntryVersion({
|
|
@@ -8,4 +8,6 @@ export declare const createEntrySchema: z.ZodObject<{
|
|
|
8
8
|
}>;
|
|
9
9
|
}, z.z.core.$strip>;
|
|
10
10
|
export declare const createEntry: (data: DbEntryInsert) => Promise<DbEntry>;
|
|
11
|
-
export declare const createEntryVersion: (data: DbEntryVersionInsert
|
|
11
|
+
export declare const createEntryVersion: (data: DbEntryVersionInsert, options?: {
|
|
12
|
+
skipValidation?: boolean;
|
|
13
|
+
}) => Promise<DbEntryVersion>;
|
|
@@ -22,20 +22,27 @@ export const createEntry = async (data) => {
|
|
|
22
22
|
});
|
|
23
23
|
return newDbEntry;
|
|
24
24
|
};
|
|
25
|
-
export const createEntryVersion = async (data) => {
|
|
25
|
+
export const createEntryVersion = async (data, options) => {
|
|
26
26
|
const { user } = requireAuth();
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
let validatedData;
|
|
28
|
+
if (options?.skipValidation) {
|
|
29
|
+
validatedData = data.data;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const entry = await getDbEntryOrThrow({ id: data.entryId });
|
|
33
|
+
const config = getCMS().getBySlug(entry.slug);
|
|
34
|
+
const languages = getCMS().languages;
|
|
35
|
+
const schema = generateZodSchemaFromFields(config.fields, languages);
|
|
36
|
+
const parsedData = schema.safeParse(data.data);
|
|
37
|
+
if (!parsedData.success) {
|
|
38
|
+
throw Error('Invalid data: ' + JSON.stringify(parsedData.error.flatten()));
|
|
39
|
+
}
|
|
40
|
+
validatedData = parsedData.data;
|
|
34
41
|
}
|
|
35
42
|
const newEntryVersion = await getCMS().databaseAdapter.createEntryVersion({
|
|
36
43
|
...data,
|
|
37
44
|
createdBy: user.id,
|
|
38
|
-
data:
|
|
45
|
+
data: validatedData
|
|
39
46
|
});
|
|
40
47
|
return newEntryVersion;
|
|
41
48
|
};
|
|
@@ -89,6 +89,47 @@ export const getRawEntryOrThrow = async (options) => {
|
|
|
89
89
|
}
|
|
90
90
|
return entry;
|
|
91
91
|
};
|
|
92
|
+
function getNestedValue(obj, keys) {
|
|
93
|
+
let current = obj;
|
|
94
|
+
for (const key of keys) {
|
|
95
|
+
if (current === null || current === undefined || typeof current !== 'object')
|
|
96
|
+
return undefined;
|
|
97
|
+
current = current[key];
|
|
98
|
+
}
|
|
99
|
+
return current;
|
|
100
|
+
}
|
|
101
|
+
function matchesDataValues(data, filter, prefix = []) {
|
|
102
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
103
|
+
const path = [...prefix, key];
|
|
104
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
105
|
+
if (!matchesDataValues(data, value, path))
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const actual = getNestedValue(data, path);
|
|
110
|
+
if (actual !== value)
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
function matchesDataLike(data, filter, prefix = []) {
|
|
117
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
118
|
+
const path = [...prefix, key];
|
|
119
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
120
|
+
if (!matchesDataLike(data, value, path))
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
else if (typeof value === 'string') {
|
|
124
|
+
const actual = getNestedValue(data, path);
|
|
125
|
+
if (typeof actual !== 'string')
|
|
126
|
+
return false;
|
|
127
|
+
if (!actual.toLowerCase().includes(value.toLowerCase()))
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
92
133
|
export const getEntries = async (options = {}) => {
|
|
93
134
|
const language = options.language || getCMS().languages[0];
|
|
94
135
|
const status = options.status || 'published';
|
|
@@ -111,11 +152,9 @@ export const getEntries = async (options = {}) => {
|
|
|
111
152
|
const entryIds = dbEntries.map((entry) => entry.id);
|
|
112
153
|
// Build entries map for quick lookup
|
|
113
154
|
const entriesMap = new Map(dbEntries.map((entry) => [entry.id, entry]));
|
|
114
|
-
// Get versions
|
|
155
|
+
// Get versions WITHOUT dataValues/dataLike - filter after finding latest
|
|
115
156
|
const dbEntryVersions = await getCMS().databaseAdapter.getEntryVersions({
|
|
116
157
|
entryIds,
|
|
117
|
-
dataValues,
|
|
118
|
-
dataLike,
|
|
119
158
|
status
|
|
120
159
|
});
|
|
121
160
|
// Group versions by entryId, keeping only the latest version for each entry
|
|
@@ -136,6 +175,21 @@ export const getEntries = async (options = {}) => {
|
|
|
136
175
|
}
|
|
137
176
|
}
|
|
138
177
|
}
|
|
178
|
+
// Post-filter: apply dataValues/dataLike on latest versions only
|
|
179
|
+
if (dataValues) {
|
|
180
|
+
for (const [entryId, entry] of latestVersionsByEntry) {
|
|
181
|
+
if (!matchesDataValues(entry.version.data, dataValues)) {
|
|
182
|
+
latestVersionsByEntry.delete(entryId);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (dataLike) {
|
|
187
|
+
for (const [entryId, entry] of latestVersionsByEntry) {
|
|
188
|
+
if (!matchesDataLike(entry.version.data, dataLike)) {
|
|
189
|
+
latestVersionsByEntry.delete(entryId);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
139
193
|
// Process entries in parallel
|
|
140
194
|
const entries = await Promise.all(Array.from(latestVersionsByEntry.values()).map(async ({ version, dbEntry }) => {
|
|
141
195
|
try {
|
|
@@ -4,7 +4,7 @@ import { resolveRelationFields } from './resolveRelationFields.js';
|
|
|
4
4
|
import { resolveUrlFields } from './resolveUrlFields.js';
|
|
5
5
|
export async function populateEntryData(data, fields, language) {
|
|
6
6
|
let populatedData = await resolveRelationFields(data, fields, language);
|
|
7
|
-
populatedData = await resolveUrlFields(populatedData, fields);
|
|
7
|
+
populatedData = await resolveUrlFields(populatedData, fields, language);
|
|
8
8
|
populatedData = await resolveMediaFields(populatedData, fields);
|
|
9
9
|
populatedData = translateObject(populatedData, language);
|
|
10
10
|
return populatedData;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { EntryData, PopulatedEntryData } from '../../../types/entries.js';
|
|
2
2
|
import type { Field } from '../../../types/fields.js';
|
|
3
|
-
export declare function resolveUrlFields(data: EntryData, fields: Field[]): Promise<PopulatedEntryData>;
|
|
3
|
+
export declare function resolveUrlFields(data: EntryData, fields: Field[], language: string): Promise<PopulatedEntryData>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { urlFieldDataSchema, urlFieldDataWithRelationSchema } from '../../../schemas/field/url.js';
|
|
2
2
|
import { getEntries } from '../entries/operations/get.js';
|
|
3
|
-
export async function resolveUrlFields(data, fields) {
|
|
3
|
+
export async function resolveUrlFields(data, fields, language) {
|
|
4
4
|
const entriesIds = [];
|
|
5
5
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
6
|
const collectIds = (value, fields) => {
|
|
@@ -36,7 +36,7 @@ export async function resolveUrlFields(data, fields) {
|
|
|
36
36
|
collectIds(data, fields);
|
|
37
37
|
if (entriesIds.length === 0)
|
|
38
38
|
return data;
|
|
39
|
-
const entries = await getEntries({ ids: entriesIds });
|
|
39
|
+
const entries = await getEntries({ ids: entriesIds, language });
|
|
40
40
|
const entriesMap = Object.fromEntries(entries.map((m) => [m.id, m]));
|
|
41
41
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
42
|
const resolveValues = (value, fields) => {
|
|
@@ -293,7 +293,17 @@ export function pg(config) {
|
|
|
293
293
|
query.where(and(options.data.folderId
|
|
294
294
|
? eq(schema.mediaFilesTable.folderId, options.data.folderId)
|
|
295
295
|
: undefined, options.data.ids ? inArray(schema.mediaFilesTable.id, options.data.ids) : undefined, options.data.mimeTypes
|
|
296
|
-
?
|
|
296
|
+
? (() => {
|
|
297
|
+
const exact = options.data.mimeTypes.filter((t) => !t.endsWith('/*'));
|
|
298
|
+
const wildcards = options.data.mimeTypes
|
|
299
|
+
.filter((t) => t.endsWith('/*'))
|
|
300
|
+
.map((t) => ilike(schema.mediaFilesTable.mimeType, t.replace('*', '%')));
|
|
301
|
+
const conditions = [
|
|
302
|
+
...(exact.length ? [inArray(schema.mediaFilesTable.mimeType, exact)] : []),
|
|
303
|
+
...wildcards
|
|
304
|
+
];
|
|
305
|
+
return conditions.length ? or(...conditions) : undefined;
|
|
306
|
+
})()
|
|
297
307
|
: undefined, searchCondition));
|
|
298
308
|
}
|
|
299
309
|
return await query.orderBy(desc(schema.mediaFilesTable.createdAt));
|