Lots as always
This commit is contained in:
parent
b91159e61f
commit
18142c7f31
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
|
@ -24,12 +24,12 @@
|
|||
"dependencies": {
|
||||
"@fontsource/vollkorn": "5.0.18",
|
||||
"@payloadcms/bundler-webpack": "1.0.6",
|
||||
"@payloadcms/db-mongodb": "1.4.0",
|
||||
"@payloadcms/richtext-lexical": "0.5.2",
|
||||
"@payloadcms/db-mongodb": "1.4.3",
|
||||
"@payloadcms/richtext-lexical": "0.7.0",
|
||||
"cross-env": "7.0.3",
|
||||
"language-tags": "1.0.9",
|
||||
"luxon": "3.4.4",
|
||||
"payload": "2.9.0",
|
||||
"payload": "2.11.2",
|
||||
"sharp": "0.33.2",
|
||||
"styled-components": "6.1.8"
|
||||
},
|
||||
|
@ -38,14 +38,14 @@
|
|||
"@types/express": "4.17.21",
|
||||
"@types/language-tags": "1.0.4",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/qs": "6.9.11",
|
||||
"@types/qs": "6.9.12",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
"@types/styled-components": "5.1.34",
|
||||
"copyfiles": "2.4.1",
|
||||
"nodemon": "3.0.3",
|
||||
"npm-check-updates": "16.14.14",
|
||||
"prettier": "3.2.4",
|
||||
"ts-node": "10.9.1",
|
||||
"nodemon": "3.1.0",
|
||||
"npm-check-updates": "16.14.15",
|
||||
"prettier": "3.2.5",
|
||||
"ts-node": "10.9.2",
|
||||
"ts-unused-exports": "10.0.1",
|
||||
"typescript": "5.3.3"
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Block } from "payload/types";
|
||||
import { createEditor } from "../utils/editor";
|
||||
import { cueBlock } from "./cueBlock";
|
||||
|
||||
export const lineBlock: Block = {
|
||||
slug: "lineBlock",
|
||||
|
@ -14,7 +15,7 @@ export const lineBlock: Block = {
|
|||
admin: {
|
||||
className: "reduced-margins",
|
||||
},
|
||||
editor: createEditor({ inlines: true, lists: true, links: true }),
|
||||
editor: createEditor({ inlines: true, lists: true, links: true, blocks: [cueBlock] }),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -28,7 +28,7 @@ export const getAllEndpoint: CollectionEndpoint = {
|
|||
const eras: ChronologyEra[] = (
|
||||
await payload.find({
|
||||
collection: Collections.ChronologyEras,
|
||||
limit: 100,
|
||||
pagination: false,
|
||||
})
|
||||
).docs;
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { Where } from "payload/types";
|
||||
import { sectionBlock } from "../../blocks/sectionBlock";
|
||||
import { transcriptBlock } from "../../blocks/transcriptBlock";
|
||||
import { CollectionGroups, Collections, FileTypes, KeysTypes } from "../../constants";
|
||||
import { CollectionGroups, Collections, FileTypes } from "../../constants";
|
||||
import { backPropagationField } from "../../fields/backPropagationField/backPropagationField";
|
||||
import { fileField } from "../../fields/fileField/fileField";
|
||||
import { imageField } from "../../fields/imageField/imageField";
|
||||
import { keysField } from "../../fields/keysField/keysField";
|
||||
import { rowField } from "../../fields/rowField/rowField";
|
||||
import { slugField } from "../../fields/slugField/slugField";
|
||||
import { tagsField } from "../../fields/tagsField/tagsField";
|
||||
import { translatedFields } from "../../fields/translatedFields/translatedFields";
|
||||
import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo";
|
||||
import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping";
|
||||
|
@ -14,14 +15,13 @@ import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish";
|
|||
import { isDefined } from "../../utils/asserts";
|
||||
import { createEditor } from "../../utils/editor";
|
||||
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig";
|
||||
import { getBySlugEndpoint } from "./endpoints/getBySlugEndpoint";
|
||||
import { importFromStrapi } from "./endpoints/importFromStrapi";
|
||||
import { importRelationsFromStrapi } from "./endpoints/importRelationsFromStrapi";
|
||||
|
||||
const fields = {
|
||||
slug: "slug",
|
||||
thumbnail: "thumbnail",
|
||||
categories: "categories",
|
||||
type: "type",
|
||||
translations: "translations",
|
||||
pretitle: "pretitle",
|
||||
title: "title",
|
||||
|
@ -42,6 +42,7 @@ const fields = {
|
|||
nextContents: "nextContents",
|
||||
folders: "folders",
|
||||
libraryItems: "libraryItems",
|
||||
tags: "tags",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
export const Contents = buildVersionedCollectionConfig({
|
||||
|
@ -55,14 +56,7 @@ export const Contents = buildVersionedCollectionConfig({
|
|||
useAsTitle: fields.slug,
|
||||
description:
|
||||
"All the contents (textual, audio, and video) from the Library or other online sources.",
|
||||
defaultColumns: [
|
||||
fields.thumbnail,
|
||||
fields.slug,
|
||||
fields.categories,
|
||||
fields.type,
|
||||
fields.translations,
|
||||
fields.status,
|
||||
],
|
||||
defaultColumns: [fields.thumbnail, fields.slug, fields.translations, fields.status],
|
||||
group: CollectionGroups.Collections,
|
||||
hooks: {
|
||||
beforeDuplicate: beforeDuplicatePiping([
|
||||
|
@ -71,7 +65,7 @@ export const Contents = buildVersionedCollectionConfig({
|
|||
]),
|
||||
},
|
||||
},
|
||||
endpoints: [importFromStrapi, importRelationsFromStrapi],
|
||||
endpoints: [importFromStrapi, importRelationsFromStrapi, getBySlugEndpoint],
|
||||
fields: [
|
||||
rowField([
|
||||
slugField({ name: fields.slug }),
|
||||
|
@ -80,23 +74,7 @@ export const Contents = buildVersionedCollectionConfig({
|
|||
relationTo: Collections.ContentsThumbnails,
|
||||
}),
|
||||
]),
|
||||
rowField([
|
||||
keysField({
|
||||
name: fields.categories,
|
||||
relationTo: KeysTypes.Categories,
|
||||
hasMany: true,
|
||||
}),
|
||||
keysField({
|
||||
name: fields.type,
|
||||
relationTo: KeysTypes.Contents,
|
||||
}),
|
||||
backPropagationField({
|
||||
name: fields.libraryItems,
|
||||
hasMany: true,
|
||||
relationTo: Collections.LibraryItems,
|
||||
where: ({ id }) => ({ "contents.content": { equals: id } }),
|
||||
}),
|
||||
]),
|
||||
tagsField({ name: fields.tags }),
|
||||
translatedFields({
|
||||
name: fields.translations,
|
||||
admin: { useAsTitle: fields.title, hasSourceLanguage: true },
|
||||
|
@ -217,19 +195,30 @@ export const Contents = buildVersionedCollectionConfig({
|
|||
rowField([
|
||||
backPropagationField({
|
||||
name: fields.folders,
|
||||
relationTo: Collections.Folders,
|
||||
hasMany: true,
|
||||
relationTo: Collections.ContentsFolders,
|
||||
where: ({ id }) => ({ contents: { equals: id } }),
|
||||
admin: {
|
||||
description: `You can set the folder(s) from the "Contents Folders" collection`,
|
||||
},
|
||||
where: ({ id }) => ({
|
||||
and: [
|
||||
{ "files.value": { equals: id } },
|
||||
{ "files.relationTo": { equals: Collections.Contents } },
|
||||
] as Where[],
|
||||
}),
|
||||
}),
|
||||
backPropagationField({
|
||||
name: fields.libraryItems,
|
||||
hasMany: true,
|
||||
relationTo: Collections.LibraryItems,
|
||||
where: ({ id }) => ({ "contents.content": { equals: id } }),
|
||||
}),
|
||||
]),
|
||||
rowField([
|
||||
backPropagationField({
|
||||
name: fields.previousContents,
|
||||
relationTo: Collections.Contents,
|
||||
hasMany: true,
|
||||
where: ({ id }) => ({ [fields.nextContents]: { equals: id } }),
|
||||
}),
|
||||
|
||||
{
|
||||
name: fields.nextContents,
|
||||
type: "relationship",
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import { Collections } from "../../../constants";
|
||||
import { createGetByEndpoint } from "../../../endpoints/createGetByEndpoint";
|
||||
import { EndpointContent } from "../../../sdk";
|
||||
import { Content } from "../../../types/collections";
|
||||
import { isPayloadArrayType, isPayloadType, isValidPayloadImage } from "../../../utils/asserts";
|
||||
import { convertTagsToGroups } from "../../../utils/tags";
|
||||
|
||||
export const getBySlugEndpoint = createGetByEndpoint(
|
||||
Collections.Contents,
|
||||
"slug",
|
||||
({ thumbnail, slug, translations, tags }: Content): EndpointContent => ({
|
||||
slug,
|
||||
...(isValidPayloadImage(thumbnail) ? { thumbnail } : {}),
|
||||
tagGroups: convertTagsToGroups(tags),
|
||||
translations: translations.map((translation) => {
|
||||
const { language, sourceLanguage, title, subtitle, pretitle, summary } = translation;
|
||||
const text = handleTextContent(translation);
|
||||
|
||||
return {
|
||||
language: isPayloadType(language) ? language.id : language,
|
||||
sourceLanguage: isPayloadType(sourceLanguage) ? sourceLanguage.id : sourceLanguage,
|
||||
...(pretitle ? { pretitle } : {}),
|
||||
title,
|
||||
...(subtitle ? { subtitle } : {}),
|
||||
...(summary ? { summary } : {}),
|
||||
format: { ...(text ? { text } : {}) },
|
||||
};
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
const handleTextContent = ({
|
||||
textContent,
|
||||
textNotes,
|
||||
textProofreaders,
|
||||
textTranscribers,
|
||||
textTranslators,
|
||||
}: Content["translations"][number]): EndpointContent["translations"][number]["format"]["text"] => {
|
||||
if (!textContent) return undefined;
|
||||
|
||||
return {
|
||||
content: textContent,
|
||||
toc: [],
|
||||
translators: isPayloadArrayType(textTranslators) ? textTranslators.map(({ id }) => id) : [],
|
||||
transcribers: isPayloadArrayType(textTranscribers) ? textTranscribers.map(({ id }) => id) : [],
|
||||
proofreaders: isPayloadArrayType(textProofreaders) ? textProofreaders.map(({ id }) => id) : [],
|
||||
...(textNotes ? { notes: textNotes } : {}),
|
||||
};
|
||||
};
|
|
@ -1,56 +0,0 @@
|
|||
import { CollectionGroups, Collections } from "../../constants";
|
||||
import { rowField } from "../../fields/rowField/rowField";
|
||||
import { slugField } from "../../fields/slugField/slugField";
|
||||
import { translatedFields } from "../../fields/translatedFields/translatedFields";
|
||||
import { buildCollectionConfig } from "../../utils/collectionConfig";
|
||||
import { importFromStrapi } from "./endpoints/importFromStrapi";
|
||||
|
||||
const fields = {
|
||||
slug: "slug",
|
||||
translations: "translations",
|
||||
name: "name",
|
||||
subfolders: "subfolders",
|
||||
contents: "contents",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
export const ContentsFolders = buildCollectionConfig({
|
||||
slug: Collections.ContentsFolders,
|
||||
labels: {
|
||||
singular: "Contents Folder",
|
||||
plural: "Contents Folders",
|
||||
},
|
||||
defaultSort: fields.slug,
|
||||
admin: {
|
||||
useAsTitle: fields.slug,
|
||||
defaultColumns: [fields.slug, fields.translations],
|
||||
disableDuplicate: true,
|
||||
group: CollectionGroups.Collections,
|
||||
},
|
||||
endpoints: [importFromStrapi],
|
||||
timestamps: false,
|
||||
versions: false,
|
||||
fields: [
|
||||
slugField({ name: fields.slug }),
|
||||
translatedFields({
|
||||
name: fields.translations,
|
||||
admin: {
|
||||
useAsTitle: fields.name,
|
||||
},
|
||||
fields: [{ name: fields.name, type: "text", required: true }],
|
||||
}),
|
||||
rowField([
|
||||
{
|
||||
type: "relationship",
|
||||
name: fields.subfolders,
|
||||
relationTo: Collections.ContentsFolders,
|
||||
hasMany: true,
|
||||
},
|
||||
{
|
||||
type: "relationship",
|
||||
name: fields.contents,
|
||||
relationTo: Collections.Contents,
|
||||
hasMany: true,
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
|
@ -1,125 +0,0 @@
|
|||
import payload from "payload";
|
||||
import QueryString from "qs";
|
||||
import { Collections } from "../../../constants";
|
||||
import { CollectionEndpoint } from "../../../types/payload";
|
||||
import { StrapiLanguage } from "../../../types/strapi";
|
||||
import { isUndefined } from "../../../utils/asserts";
|
||||
import { findContent } from "../../../utils/localApi";
|
||||
|
||||
type StrapiContentsFolder = {
|
||||
id: string;
|
||||
attributes: {
|
||||
slug: string;
|
||||
titles?: { title: string; language: StrapiLanguage }[];
|
||||
subfolders: { data: StrapiContentsFolder[] };
|
||||
contents: { data: { attributes: { slug: string } }[] };
|
||||
};
|
||||
};
|
||||
|
||||
const getStrapiContentFolder = async (id: number): Promise<StrapiContentsFolder> => {
|
||||
const paramsWithPagination = QueryString.stringify({
|
||||
populate: [
|
||||
"contents",
|
||||
"subfolders",
|
||||
"subfolders.contents",
|
||||
"subfolders.titles",
|
||||
"subfolders.titles.language",
|
||||
"subfolders.subfolders",
|
||||
"subfolders.subfolders.contents",
|
||||
"subfolders.subfolders.titles",
|
||||
"subfolders.subfolders.titles.language",
|
||||
"subfolders.subfolders.subfolders",
|
||||
"subfolders.subfolders.subfolders.contents",
|
||||
"subfolders.subfolders.subfolders.titles",
|
||||
"subfolders.subfolders.subfolders.titles.language",
|
||||
"subfolders.subfolders.subfolders.subfolders",
|
||||
"subfolders.subfolders.subfolders.subfolders.contents",
|
||||
"subfolders.subfolders.subfolders.subfolders.titles",
|
||||
"subfolders.subfolders.subfolders.subfolders.titles.language",
|
||||
"subfolders.subfolders.subfolders.subfolders.subfolders",
|
||||
"subfolders.subfolders.subfolders.subfolders.subfolders.contents",
|
||||
"subfolders.subfolders.subfolders.subfolders.subfolders.titles",
|
||||
"subfolders.subfolders.subfolders.subfolders.subfolders.titles.language",
|
||||
"subfolders.subfolders.subfolders.subfolders.subfolders.subfolders",
|
||||
"subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.contents",
|
||||
"subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.titles",
|
||||
"subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.titles.language",
|
||||
"subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.subfolders",
|
||||
"subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.contents",
|
||||
"subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.titles",
|
||||
"subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.subfolders.titles.language",
|
||||
],
|
||||
});
|
||||
const uri = `${process.env.STRAPI_URI}/api/contents-folders/${id}?${paramsWithPagination}`;
|
||||
const fetchResult = await fetch(uri, {
|
||||
method: "get",
|
||||
headers: { authorization: `Bearer ${process.env.STRAPI_TOKEN}` },
|
||||
});
|
||||
const { data } = await fetchResult.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
export const importFromStrapi: CollectionEndpoint = {
|
||||
method: "post",
|
||||
path: "/strapi",
|
||||
handler: async (req, res) => {
|
||||
if (!req.user) {
|
||||
return res.status(403).send({
|
||||
errors: [
|
||||
{
|
||||
message: "You are not allowed to perform this action.",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
let foldersCreated = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
const createContentFolder = async (data: StrapiContentsFolder): Promise<string> => {
|
||||
const { slug, titles } = data.attributes;
|
||||
|
||||
const subfolders = await Promise.all(
|
||||
data.attributes.subfolders.data.map(createContentFolder)
|
||||
);
|
||||
|
||||
const contents: string[] = [];
|
||||
for (const content of data.attributes.contents.data) {
|
||||
try {
|
||||
const result = await findContent(content.attributes.slug);
|
||||
contents.push(result);
|
||||
} catch (e) {
|
||||
errors.push(`Couldn't add ${content.attributes.slug} to folder ${slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await payload.create({
|
||||
collection: Collections.ContentsFolders,
|
||||
data: {
|
||||
slug,
|
||||
subfolders,
|
||||
contents,
|
||||
translations: titles?.map(({ title, language }) => {
|
||||
if (isUndefined(language.data))
|
||||
throw new Error("A language is required for a content folder translation");
|
||||
return { language: language.data.attributes.code, name: title };
|
||||
}),
|
||||
},
|
||||
user: req.user,
|
||||
});
|
||||
foldersCreated++;
|
||||
return result.id;
|
||||
};
|
||||
|
||||
const rootFolder = await getStrapiContentFolder(72);
|
||||
try {
|
||||
await createContentFolder(rootFolder);
|
||||
} catch (e) {
|
||||
res.status(500).json({ message: "Something went wrong", error: e });
|
||||
}
|
||||
|
||||
res
|
||||
.status(200)
|
||||
.json({ message: `${foldersCreated} entries have been added successfully.`, errors });
|
||||
},
|
||||
};
|
|
@ -20,11 +20,11 @@ export const getAllEndpoint: CollectionEndpoint = {
|
|||
const currencies: Currency[] = (
|
||||
await payload.find({
|
||||
collection: Collections.Currencies,
|
||||
limit: 100,
|
||||
sort: "id"
|
||||
sort: "id",
|
||||
pagination: false,
|
||||
})
|
||||
).docs;
|
||||
|
||||
res.status(200).header("Cache-Control", "max-age=60").json(currencies);
|
||||
res.status(200).json(currencies);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -83,7 +83,7 @@ export const Folders = buildCollectionConfig({
|
|||
{
|
||||
type: "relationship",
|
||||
name: fields.files,
|
||||
relationTo: [Collections.LibraryItems, Collections.Contents],
|
||||
relationTo: [Collections.LibraryItems, Collections.Contents, Collections.Pages],
|
||||
hasMany: true,
|
||||
},
|
||||
],
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Collections } from "../../../constants";
|
||||
import { createGetByEndpoint } from "../../../endpoints/createGetByEndpoint";
|
||||
import { EndpointFolder, EndpointFolderPreview, PayloadImage } from "../../../sdk";
|
||||
import { Folder, FoldersThumbnail, Language } from "../../../types/collections";
|
||||
import { isDefined, isUndefined, isValidPayloadImage } from "../../../utils/asserts";
|
||||
import { EndpointFolder, EndpointFolderPreview } from "../../../sdk";
|
||||
import { Folder, Language } from "../../../types/collections";
|
||||
import { isDefined, isPayloadType, isValidPayloadImage } from "../../../utils/asserts";
|
||||
|
||||
export const getBySlugEndpoint = createGetByEndpoint(
|
||||
Collections.Folders,
|
||||
|
@ -15,7 +15,7 @@ export const getBySlugEndpoint = createGetByEndpoint(
|
|||
? {
|
||||
type: "single",
|
||||
subfolders:
|
||||
folder.sections[0]?.subfolders?.filter(isValidFolder).map(convertFolderToPreview) ??
|
||||
folder.sections[0]?.subfolders?.filter(isPayloadType).map(convertFolderToPreview) ??
|
||||
[],
|
||||
}
|
||||
: {
|
||||
|
@ -29,6 +29,20 @@ export const getBySlugEndpoint = createGetByEndpoint(
|
|||
subfolders: subfolders.map(convertFolderToPreview),
|
||||
})) ?? [],
|
||||
},
|
||||
files:
|
||||
folder.files?.flatMap<EndpointFolder["files"][number]>(({ relationTo, value }) => {
|
||||
if (!isPayloadType(value)) {
|
||||
return [];
|
||||
}
|
||||
switch (relationTo) {
|
||||
case "contents":
|
||||
return [{ relationTo, value }];
|
||||
case "library-items":
|
||||
return [{ relationTo, value }];
|
||||
case "pages":
|
||||
return [{ relationTo, value }];
|
||||
}
|
||||
}) ?? [],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -47,10 +61,10 @@ export const convertFolderToPreview = ({
|
|||
translations?.map(({ language, name, description }) => ({
|
||||
language: getLanguageId(language),
|
||||
name,
|
||||
description: JSON.stringify(description),
|
||||
...(description ? { description } : {}),
|
||||
})) ?? [],
|
||||
darkThumbnail: getThumbnail(darkThumbnail),
|
||||
lightThumbnail: getThumbnail(lightThumbnail),
|
||||
darkThumbnail: isValidPayloadImage(darkThumbnail) ? darkThumbnail : undefined,
|
||||
lightThumbnail: isValidPayloadImage(lightThumbnail) ? lightThumbnail : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -77,19 +91,7 @@ const isValidSection = (section: {
|
|||
if (!section.subfolders) {
|
||||
return false;
|
||||
}
|
||||
return section.subfolders.every(isValidFolder);
|
||||
};
|
||||
|
||||
export const isValidFolder = (folder: string | Folder): folder is Folder =>
|
||||
typeof folder !== "string";
|
||||
|
||||
const getThumbnail = (
|
||||
thumbnail: string | FoldersThumbnail | null | undefined
|
||||
): PayloadImage | undefined => {
|
||||
if (isUndefined(thumbnail)) return undefined;
|
||||
if (typeof thumbnail === "string") return undefined;
|
||||
if (!isValidPayloadImage(thumbnail)) return undefined;
|
||||
return thumbnail;
|
||||
return section.subfolders.every(isPayloadType);
|
||||
};
|
||||
|
||||
const getLanguageId = (language: string | Language) =>
|
||||
|
|
|
@ -2,7 +2,8 @@ import payload from "payload";
|
|||
import { Collections } from "../../../constants";
|
||||
import { EndpointFolderPreview } from "../../../sdk";
|
||||
import { CollectionEndpoint } from "../../../types/payload";
|
||||
import { convertFolderToPreview, isValidFolder } from "./getBySlugEndpoint";
|
||||
import { isPayloadType } from "../../../utils/asserts";
|
||||
import { convertFolderToPreview } from "./getBySlugEndpoint";
|
||||
|
||||
|
||||
export const getRootFoldersEndpoint: CollectionEndpoint = {
|
||||
|
@ -39,7 +40,7 @@ export const getRootFoldersEndpoint: CollectionEndpoint = {
|
|||
return;
|
||||
}
|
||||
|
||||
const result = folders.filter(isValidFolder).map<EndpointFolderPreview>(convertFolderToPreview);
|
||||
const result = folders.filter(isPayloadType).map<EndpointFolderPreview>(convertFolderToPreview);
|
||||
|
||||
res.status(200).json(result);
|
||||
},
|
||||
|
|
|
@ -16,18 +16,7 @@ export const FoldersThumbnails = buildImageCollectionConfig({
|
|||
},
|
||||
admin: { defaultColumns: [fields.filename, fields.updatedAt] },
|
||||
upload: {
|
||||
imageSizes: [
|
||||
{
|
||||
name: "medium",
|
||||
height: 400,
|
||||
width: 200,
|
||||
fit: "contain",
|
||||
formatOptions: {
|
||||
format: "webp",
|
||||
options: { effort: 6, quality: 80, alphaQuality: 80 },
|
||||
},
|
||||
},
|
||||
],
|
||||
imageSizes: [],
|
||||
},
|
||||
fields: [],
|
||||
});
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { Collections } from "../../constants";
|
||||
import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig";
|
||||
|
||||
const fields = {
|
||||
filename: "filename",
|
||||
mimeType: "mimeType",
|
||||
filesize: "filesize",
|
||||
posts: "posts",
|
||||
updatedAt: "updatedAt",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
export const Images = buildImageCollectionConfig({
|
||||
slug: Collections.Images,
|
||||
labels: {
|
||||
singular: "Image",
|
||||
plural: "Images",
|
||||
},
|
||||
admin: { defaultColumns: [fields.filename, fields.posts, fields.updatedAt] },
|
||||
upload: {
|
||||
imageSizes: [
|
||||
{
|
||||
name: "og",
|
||||
height: 750,
|
||||
width: 1125,
|
||||
formatOptions: {
|
||||
format: "jpg",
|
||||
options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 60 },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
fields: [],
|
||||
});
|
|
@ -8,6 +8,7 @@ import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo";
|
|||
import { Key } from "../../types/collections";
|
||||
import { isDefined, isUndefined } from "../../utils/asserts";
|
||||
import { buildCollectionConfig } from "../../utils/collectionConfig";
|
||||
import { getAllEndpoint } from "./endpoints/getAllEndpoint";
|
||||
import { importFromStrapi } from "./endpoints/importFromStrapi";
|
||||
|
||||
const fields = {
|
||||
|
@ -74,7 +75,7 @@ export const Keys = buildCollectionConfig({
|
|||
},
|
||||
],
|
||||
},
|
||||
endpoints: [importFromStrapi],
|
||||
endpoints: [importFromStrapi, getAllEndpoint],
|
||||
timestamps: false,
|
||||
versions: false,
|
||||
fields: [
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import payload from "payload";
|
||||
import { Collections } from "../../../constants";
|
||||
import { EndpointKey } from "../../../sdk";
|
||||
import { Key } from "../../../types/collections";
|
||||
import { CollectionEndpoint } from "../../../types/payload";
|
||||
import { isPayloadType } from "../../../utils/asserts";
|
||||
|
||||
export const getAllEndpoint: CollectionEndpoint = {
|
||||
method: "get",
|
||||
path: "/all",
|
||||
handler: async (req, res) => {
|
||||
if (!req.user) {
|
||||
return res.status(403).send({
|
||||
errors: [
|
||||
{
|
||||
message: "You are not allowed to perform this action.",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const keys: Key[] = (
|
||||
await payload.find({
|
||||
collection: Collections.Keys,
|
||||
sort: "id",
|
||||
pagination: false,
|
||||
})
|
||||
).docs;
|
||||
|
||||
const result: EndpointKey[] = keys.map(({ translations, ...others }) => ({
|
||||
...others,
|
||||
translations:
|
||||
translations?.map(({ language, name, short }) => ({
|
||||
language: isPayloadType(language) ? language.id : language,
|
||||
name,
|
||||
short: short ?? name,
|
||||
})) ?? [],
|
||||
}));
|
||||
|
||||
res.status(200).json(result);
|
||||
},
|
||||
};
|
|
@ -20,8 +20,8 @@ export const getAllEndpoint: CollectionEndpoint = {
|
|||
const languages: Language[] = (
|
||||
await payload.find({
|
||||
collection: Collections.Languages,
|
||||
limit: 100,
|
||||
sort: "name"
|
||||
sort: "name",
|
||||
pagination: false,
|
||||
})
|
||||
).docs;
|
||||
|
||||
|
|
|
@ -0,0 +1,215 @@
|
|||
import { Where } from "payload/types";
|
||||
import { sectionBlock } from "../../blocks/sectionBlock";
|
||||
import { QuickFilters, publishStatusFilters } from "../../components/QuickFilters";
|
||||
import { CollectionGroups, Collections, PageType } from "../../constants";
|
||||
import { backPropagationField } from "../../fields/backPropagationField/backPropagationField";
|
||||
import { imageField } from "../../fields/imageField/imageField";
|
||||
import { rowField } from "../../fields/rowField/rowField";
|
||||
import { slugField } from "../../fields/slugField/slugField";
|
||||
import { tagsField } from "../../fields/tagsField/tagsField";
|
||||
import { translatedFields } from "../../fields/translatedFields/translatedFields";
|
||||
import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo";
|
||||
import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping";
|
||||
import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish";
|
||||
import { isDefined, isUndefined } from "../../utils/asserts";
|
||||
import { createEditor } from "../../utils/editor";
|
||||
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig";
|
||||
import { getBySlugEndpoint } from "./endpoints/getBySlugEndpoint";
|
||||
|
||||
const fields = {
|
||||
slug: "slug",
|
||||
type: "type",
|
||||
authors: "authors",
|
||||
thumbnail: "thumbnail",
|
||||
translations: "translations",
|
||||
tags: "tags",
|
||||
sourceLanguage: "sourceLanguage",
|
||||
pretitle: "pretitle",
|
||||
title: "title",
|
||||
subtitle: "subtitle",
|
||||
summary: "summary",
|
||||
content: "content",
|
||||
transcribers: "transcribers",
|
||||
translators: "translators",
|
||||
proofreaders: "proofreaders",
|
||||
collectibles: "collectibles",
|
||||
folders: "folders",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
const pageTypesWithAuthor = [PageType.Article];
|
||||
const pageTypesWithCollectibles = [PageType.Content];
|
||||
const pageTypesWithTranscribers = [PageType.Content];
|
||||
|
||||
export const Pages = buildVersionedCollectionConfig({
|
||||
slug: Collections.Pages,
|
||||
labels: {
|
||||
singular: "Page",
|
||||
plural: "Pages",
|
||||
},
|
||||
defaultSort: fields.slug,
|
||||
admin: {
|
||||
useAsTitle: fields.slug,
|
||||
defaultColumns: [fields.thumbnail, fields.slug],
|
||||
group: CollectionGroups.Collections,
|
||||
components: {
|
||||
BeforeListTable: [
|
||||
() =>
|
||||
QuickFilters({
|
||||
slug: Collections.Posts,
|
||||
filterGroups: [publishStatusFilters],
|
||||
}),
|
||||
],
|
||||
},
|
||||
hooks: {
|
||||
beforeDuplicate: beforeDuplicatePiping([
|
||||
beforeDuplicateUnpublish,
|
||||
beforeDuplicateAddCopyTo(fields.slug),
|
||||
]),
|
||||
},
|
||||
},
|
||||
endpoints: [getBySlugEndpoint],
|
||||
fields: [
|
||||
{
|
||||
name: fields.type,
|
||||
type: "radio",
|
||||
required: true,
|
||||
defaultValue: PageType.Generic,
|
||||
options: Object.entries(PageType).map(([value, label]) => ({ label, value })),
|
||||
},
|
||||
rowField([
|
||||
slugField({ name: fields.slug }),
|
||||
imageField({
|
||||
name: fields.thumbnail,
|
||||
relationTo: Collections.Images,
|
||||
}),
|
||||
]),
|
||||
rowField([
|
||||
tagsField({ name: fields.tags }),
|
||||
{
|
||||
name: fields.authors,
|
||||
type: "relationship",
|
||||
admin: {
|
||||
condition: (_, siblingData) => pageTypesWithAuthor.includes(siblingData[fields.type]),
|
||||
},
|
||||
relationTo: Collections.Recorders,
|
||||
required: true,
|
||||
minRows: 1,
|
||||
hasMany: true,
|
||||
},
|
||||
]),
|
||||
translatedFields({
|
||||
name: fields.translations,
|
||||
admin: { useAsTitle: fields.title, hasSourceLanguage: true },
|
||||
required: true,
|
||||
minRows: 1,
|
||||
fields: [
|
||||
rowField([
|
||||
{ name: fields.pretitle, type: "text" },
|
||||
{ name: fields.title, type: "text", required: true },
|
||||
{ name: fields.subtitle, type: "text" },
|
||||
]),
|
||||
{
|
||||
name: fields.summary,
|
||||
type: "richText",
|
||||
editor: createEditor({ inlines: true, lists: true, links: true }),
|
||||
},
|
||||
{
|
||||
name: fields.content,
|
||||
type: "richText",
|
||||
required: true,
|
||||
editor: createEditor({
|
||||
images: true,
|
||||
inlines: true,
|
||||
alignment: true,
|
||||
blocks: [sectionBlock],
|
||||
links: true,
|
||||
lists: true,
|
||||
}),
|
||||
},
|
||||
rowField([
|
||||
{
|
||||
name: fields.transcribers,
|
||||
type: "relationship",
|
||||
relationTo: Collections.Recorders,
|
||||
hasMany: true,
|
||||
admin: {
|
||||
condition: (data, siblingData) => {
|
||||
if (!pageTypesWithTranscribers.includes(data[fields.type])) {
|
||||
return false;
|
||||
}
|
||||
if (isUndefined(siblingData.language) || isUndefined(siblingData.sourceLanguage)) {
|
||||
return false;
|
||||
}
|
||||
return siblingData.language === siblingData.sourceLanguage;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: fields.translators,
|
||||
type: "relationship",
|
||||
relationTo: Collections.Recorders,
|
||||
hasMany: true,
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
({ siblingData }) => {
|
||||
if (siblingData.language === siblingData.sourceLanguage) {
|
||||
delete siblingData.translators;
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
admin: {
|
||||
condition: (_, siblingData) => {
|
||||
if (isUndefined(siblingData.language) || isUndefined(siblingData.sourceLanguage)) {
|
||||
return false;
|
||||
}
|
||||
return siblingData.language !== siblingData.sourceLanguage;
|
||||
},
|
||||
},
|
||||
validate: (translators, { siblingData }) => {
|
||||
if (isUndefined(siblingData.language) || isUndefined(siblingData.sourceLanguage)) {
|
||||
return true;
|
||||
}
|
||||
if (siblingData.language === siblingData.sourceLanguage) {
|
||||
return true;
|
||||
}
|
||||
if (isDefined(translators) && translators.length > 0) {
|
||||
return true;
|
||||
}
|
||||
return "This field is required when the language is different from the source language.";
|
||||
},
|
||||
},
|
||||
{
|
||||
name: fields.proofreaders,
|
||||
type: "relationship",
|
||||
relationTo: Collections.Recorders,
|
||||
hasMany: true,
|
||||
},
|
||||
]),
|
||||
],
|
||||
}),
|
||||
rowField([
|
||||
backPropagationField({
|
||||
name: fields.folders,
|
||||
relationTo: Collections.Folders,
|
||||
hasMany: true,
|
||||
where: ({ id }) => ({
|
||||
and: [
|
||||
{ "files.value": { equals: id } },
|
||||
{ "files.relationTo": { equals: Collections.Pages } },
|
||||
] as Where[],
|
||||
}),
|
||||
}),
|
||||
backPropagationField({
|
||||
name: fields.collectibles,
|
||||
hasMany: true,
|
||||
relationTo: Collections.LibraryItems,
|
||||
admin: {
|
||||
condition: (_, siblingData) =>
|
||||
pageTypesWithCollectibles.includes(siblingData[fields.type]),
|
||||
},
|
||||
where: ({ id }) => ({ "contents.content": { equals: id } }),
|
||||
}),
|
||||
]),
|
||||
],
|
||||
});
|
|
@ -0,0 +1,134 @@
|
|||
import {
|
||||
Collections,
|
||||
PageType,
|
||||
RichTextContent,
|
||||
isBlockNodeSectionBlock,
|
||||
isNodeBlockNode,
|
||||
} from "../../../constants";
|
||||
import { createGetByEndpoint } from "../../../endpoints/createGetByEndpoint";
|
||||
import { EndpointPage, ParentPage, TableOfContentEntry } from "../../../sdk";
|
||||
import { Page } from "../../../types/collections";
|
||||
import { isPayloadArrayType, isPayloadType, isValidPayloadImage } from "../../../utils/asserts";
|
||||
import { convertTagsToGroups } from "../../../utils/tags";
|
||||
|
||||
export const getBySlugEndpoint = createGetByEndpoint(
|
||||
Collections.Pages,
|
||||
"slug",
|
||||
({
|
||||
authors,
|
||||
slug,
|
||||
translations,
|
||||
tags,
|
||||
thumbnail,
|
||||
_status,
|
||||
collectibles,
|
||||
folders,
|
||||
type,
|
||||
}: Page): EndpointPage => ({
|
||||
slug,
|
||||
type: type as PageType,
|
||||
...(isValidPayloadImage(thumbnail) ? { thumbnail } : {}),
|
||||
tagGroups: convertTagsToGroups(tags),
|
||||
translations: translations.map(
|
||||
({
|
||||
content,
|
||||
language,
|
||||
sourceLanguage,
|
||||
title,
|
||||
pretitle,
|
||||
subtitle,
|
||||
proofreaders,
|
||||
summary,
|
||||
transcribers,
|
||||
translators,
|
||||
}) => ({
|
||||
language: isPayloadType(language) ? language.id : language,
|
||||
sourceLanguage: isPayloadType(sourceLanguage) ? sourceLanguage.id : sourceLanguage,
|
||||
...(pretitle ? { pretitle } : {}),
|
||||
title,
|
||||
...(subtitle ? { subtitle } : {}),
|
||||
...(summary ? { summary } : {}),
|
||||
content: handleContent(content),
|
||||
toc: handleToc(content),
|
||||
translators: isPayloadArrayType(translators) ? translators.map(({ id }) => id) : [],
|
||||
transcribers: isPayloadArrayType(transcribers) ? transcribers.map(({ id }) => id) : [],
|
||||
proofreaders: isPayloadArrayType(proofreaders) ? proofreaders.map(({ id }) => id) : [],
|
||||
})
|
||||
),
|
||||
authors: isPayloadArrayType(authors) ? authors.map(({ id }) => id) : [],
|
||||
status: _status === "published" ? "published" : "draft",
|
||||
parentPages: handleParentPages({ collectibles, folders }),
|
||||
})
|
||||
);
|
||||
|
||||
const handleContent = (
|
||||
{ root: { children, ...others } }: RichTextContent,
|
||||
parentPrefix = ""
|
||||
): RichTextContent => {
|
||||
let sectionCount = 0;
|
||||
return {
|
||||
root: {
|
||||
...others,
|
||||
children: children.map((node) => {
|
||||
if (isNodeBlockNode(node) && isBlockNodeSectionBlock(node)) {
|
||||
sectionCount++;
|
||||
const anchorHash = `${parentPrefix}${sectionCount}.`;
|
||||
return {
|
||||
...node,
|
||||
fields: {
|
||||
...node.fields,
|
||||
anchorHash,
|
||||
lines: handleContent(node.fields.lines, anchorHash),
|
||||
},
|
||||
};
|
||||
}
|
||||
return node;
|
||||
}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const handleToc = (content: RichTextContent, parentPrefix = ""): TableOfContentEntry[] =>
|
||||
content.root.children
|
||||
.filter(isNodeBlockNode)
|
||||
.filter(isBlockNodeSectionBlock)
|
||||
.map(({ fields }, index) => ({
|
||||
prefix: `${parentPrefix}${index + 1}.`,
|
||||
title: fields.blockName,
|
||||
children: handleToc(fields.lines, `${index + 1}.`),
|
||||
}));
|
||||
|
||||
const handleParentPages = ({
|
||||
collectibles,
|
||||
folders,
|
||||
}: Pick<Page, "collectibles" | "folders">): ParentPage[] => {
|
||||
const result: ParentPage[] = [];
|
||||
|
||||
if (collectibles && isPayloadArrayType(collectibles)) {
|
||||
collectibles.forEach(({ slug, title }) => {
|
||||
result.push({
|
||||
collection: Collections.LibraryItems,
|
||||
slug,
|
||||
translations: [{ language: "en", name: title }],
|
||||
tag: "collectible",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (folders && isPayloadArrayType(folders)) {
|
||||
folders.forEach(({ slug, translations }) => {
|
||||
result.push({
|
||||
collection: Collections.Folders,
|
||||
slug,
|
||||
translations:
|
||||
translations?.map(({ language, name }) => ({
|
||||
language: isPayloadType(language) ? language.id : language,
|
||||
name,
|
||||
})) ?? [],
|
||||
tag: "folders",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
|
@ -8,6 +8,7 @@ import { rowField } from "../../fields/rowField/rowField";
|
|||
import { translatedFields } from "../../fields/translatedFields/translatedFields";
|
||||
import { buildCollectionConfig } from "../../utils/collectionConfig";
|
||||
import { createEditor } from "../../utils/editor";
|
||||
import { getAllEndpoint } from "./endpoints/getAllEndpoint";
|
||||
import { importFromStrapi } from "./endpoints/importFromStrapi";
|
||||
import { beforeLoginMustHaveAtLeastOneRole } from "./hooks/beforeLoginMustHaveAtLeastOneRole";
|
||||
|
||||
|
@ -75,7 +76,7 @@ export const Recorders = buildCollectionConfig({
|
|||
hooks: {
|
||||
beforeLogin: [beforeLoginMustHaveAtLeastOneRole],
|
||||
},
|
||||
endpoints: [importFromStrapi],
|
||||
endpoints: [importFromStrapi, getAllEndpoint],
|
||||
timestamps: false,
|
||||
fields: [
|
||||
rowField([
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import payload from "payload";
|
||||
import { Collections } from "../../../constants";
|
||||
import { EndpointRecorder } from "../../../sdk";
|
||||
import { CollectionEndpoint } from "../../../types/payload";
|
||||
import { isPayloadArrayType, isPayloadType, isValidPayloadImage } from "../../../utils/asserts";
|
||||
|
||||
export const getAllEndpoint: CollectionEndpoint = {
|
||||
method: "get",
|
||||
path: "/all",
|
||||
handler: async (req, res) => {
|
||||
if (!req.user) {
|
||||
return res.status(403).send({
|
||||
errors: [
|
||||
{
|
||||
message: "You are not allowed to perform this action.",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const recorders = (
|
||||
await payload.find({
|
||||
collection: Collections.Recorders,
|
||||
sort: "id",
|
||||
pagination: false,
|
||||
})
|
||||
).docs;
|
||||
|
||||
const result: EndpointRecorder[] = recorders.map(
|
||||
({ anonymize, id, username, avatar, biographies, languages }) => ({
|
||||
id,
|
||||
username: anonymize ? `Recorder#${id.substring(0, 5)}` : username,
|
||||
...(isValidPayloadImage(avatar) ? { avatar } : {}),
|
||||
languages: isPayloadArrayType(languages) ? languages.map(({ id }) => id) : [],
|
||||
biographies:
|
||||
biographies?.map(({ biography, language }) => ({
|
||||
language: isPayloadType(language) ? language.id : language,
|
||||
biography,
|
||||
})) ?? [],
|
||||
})
|
||||
);
|
||||
|
||||
res.status(200).json(result);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
import payload from "payload";
|
||||
import { CollectionBeforeChangeHook, CollectionConfig } from "payload/types";
|
||||
import { CollectionGroups, Collections } from "../../constants";
|
||||
import { slugField } from "../../fields/slugField/slugField";
|
||||
import { translatedFields } from "../../fields/translatedFields/translatedFields";
|
||||
import { buildCollectionConfig } from "../../utils/collectionConfig";
|
||||
import { getAllEndpoint } from "./endpoints/getAllEndpoint";
|
||||
|
||||
const beforeChangeUpdateName: CollectionBeforeChangeHook = async ({ data }) => {
|
||||
let name = data.slug;
|
||||
|
||||
const parentId = data[fields.group];
|
||||
|
||||
if (parentId) {
|
||||
const parent = await payload.findByID({
|
||||
collection: Collections.TagsGroups,
|
||||
id: data[fields.group],
|
||||
});
|
||||
name = `${parent.slug} / ${data.slug}`;
|
||||
}
|
||||
|
||||
return {
|
||||
...data,
|
||||
[fields.name]: name,
|
||||
};
|
||||
};
|
||||
|
||||
const fields = {
|
||||
slug: "slug",
|
||||
name: "name",
|
||||
translations: "translations",
|
||||
translationsName: "name",
|
||||
group: "group",
|
||||
};
|
||||
|
||||
export const Tags: CollectionConfig = buildCollectionConfig({
|
||||
slug: Collections.Tags,
|
||||
labels: { singular: "Tag", plural: "Tags" },
|
||||
admin: {
|
||||
group: CollectionGroups.Meta,
|
||||
useAsTitle: fields.name,
|
||||
defaultColumns: [fields.slug, fields.group, fields.translations],
|
||||
},
|
||||
endpoints: [getAllEndpoint],
|
||||
hooks: { beforeChange: [beforeChangeUpdateName] },
|
||||
fields: [
|
||||
{ name: fields.name, type: "text", admin: { readOnly: true, hidden: true } },
|
||||
slugField({ name: fields.slug }),
|
||||
translatedFields({
|
||||
name: fields.translations,
|
||||
admin: { useAsTitle: fields.translationsName },
|
||||
fields: [{ name: fields.translationsName, type: "text", required: true }],
|
||||
}),
|
||||
{ name: fields.group, type: "relationship", required: true, relationTo: Collections.TagsGroups },
|
||||
],
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
import payload from "payload";
|
||||
import { Collections } from "../../../constants";
|
||||
import { EndpointTag } from "../../../sdk";
|
||||
import { CollectionEndpoint } from "../../../types/payload";
|
||||
import { isPayloadType } from "../../../utils/asserts";
|
||||
|
||||
export const getAllEndpoint: CollectionEndpoint = {
|
||||
method: "get",
|
||||
path: "/all",
|
||||
handler: async (req, res) => {
|
||||
if (!req.user) {
|
||||
return res.status(403).send({
|
||||
errors: [
|
||||
{
|
||||
message: "You are not allowed to perform this action.",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const tags = (
|
||||
await payload.find({
|
||||
collection: Collections.Tags,
|
||||
sort: "id",
|
||||
pagination: false,
|
||||
})
|
||||
).docs;
|
||||
|
||||
const result = tags.map<EndpointTag>(({ slug, translations, group }) => ({
|
||||
slug,
|
||||
translations:
|
||||
translations?.map(({ language, name }) => ({
|
||||
language: isPayloadType(language) ? language.id : language,
|
||||
name,
|
||||
})) ?? [],
|
||||
group: isPayloadType(group) ? group.slug : group,
|
||||
}));
|
||||
|
||||
res.status(200).json(result);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
import { CollectionConfig } from "payload/types";
|
||||
import { CollectionGroups, Collections } from "../../constants";
|
||||
import { iconField } from "../../fields/iconField/iconField";
|
||||
import { slugField } from "../../fields/slugField/slugField";
|
||||
import { translatedFields } from "../../fields/translatedFields/translatedFields";
|
||||
import { buildCollectionConfig } from "../../utils/collectionConfig";
|
||||
import { getAllEndpoint } from "./endpoints/getAllEndpoint";
|
||||
|
||||
const fields = {
|
||||
slug: "slug",
|
||||
translations: "translations",
|
||||
translationsName: "name",
|
||||
icon: "icon",
|
||||
};
|
||||
|
||||
export const TagsGroups: CollectionConfig = buildCollectionConfig({
|
||||
slug: Collections.TagsGroups,
|
||||
labels: { singular: "Tags Group", plural: "Tags Groups" },
|
||||
admin: {
|
||||
group: CollectionGroups.Meta,
|
||||
useAsTitle: fields.slug,
|
||||
defaultColumns: [fields.slug, fields.translations],
|
||||
},
|
||||
endpoints: [getAllEndpoint],
|
||||
fields: [
|
||||
slugField({ name: fields.slug }),
|
||||
iconField({ name: fields.icon }),
|
||||
translatedFields({
|
||||
name: fields.translations,
|
||||
admin: { useAsTitle: fields.translationsName },
|
||||
fields: [{ name: fields.translationsName, type: "text", required: true }],
|
||||
}),
|
||||
],
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
import payload from "payload";
|
||||
import { Collections } from "../../../constants";
|
||||
import { EndpointTagsGroup } from "../../../sdk";
|
||||
import { CollectionEndpoint } from "../../../types/payload";
|
||||
import { isPayloadType } from "../../../utils/asserts";
|
||||
|
||||
export const getAllEndpoint: CollectionEndpoint = {
|
||||
method: "get",
|
||||
path: "/all",
|
||||
handler: async (req, res) => {
|
||||
if (!req.user) {
|
||||
return res.status(403).send({
|
||||
errors: [
|
||||
{
|
||||
message: "You are not allowed to perform this action.",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const tags = (
|
||||
await payload.find({
|
||||
collection: Collections.TagsGroups,
|
||||
sort: "id",
|
||||
pagination: false,
|
||||
})
|
||||
).docs;
|
||||
|
||||
const result = tags.map<EndpointTagsGroup>(({ slug, translations, icon }) => ({
|
||||
slug,
|
||||
...(icon ? { icon } : {}),
|
||||
translations:
|
||||
translations?.map(({ language, name }) => ({
|
||||
language: isPayloadType(language) ? language.id : language,
|
||||
name,
|
||||
})) ?? [],
|
||||
tags: [] // TODO: Add tags,
|
||||
}));
|
||||
|
||||
res.status(200).json(result);
|
||||
},
|
||||
};
|
164
src/constants.ts
164
src/constants.ts
|
@ -1,7 +1,10 @@
|
|||
import { CueBlock, LineBlock, SectionBlock, TranscriptBlock } from "./types/collections";
|
||||
|
||||
// END MOCKING SECTION
|
||||
|
||||
export enum Collections {
|
||||
ChronologyEras = "chronology-eras",
|
||||
ChronologyItems = "chronology-items",
|
||||
ContentsFolders = "contents-folders",
|
||||
Contents = "contents",
|
||||
ContentsThumbnails = "contents-thumbnails",
|
||||
Currencies = "currencies",
|
||||
|
@ -12,8 +15,10 @@ export enum Collections {
|
|||
LibraryItemsThumbnails = "library-items-thumbnails",
|
||||
LibraryItemsScans = "library-items-scans",
|
||||
LibraryItemsGallery = "library-items-gallery",
|
||||
Notes = "Notes",
|
||||
Notes = "notes",
|
||||
Posts = "posts",
|
||||
Pages = "pages",
|
||||
PagesThumbnails = "pages-thumbnails",
|
||||
PostsThumbnails = "posts-thumbnails",
|
||||
Recorders = "recorders",
|
||||
RecordersThumbnails = "recorders-thumbnails",
|
||||
|
@ -23,7 +28,10 @@ export enum Collections {
|
|||
WeaponsGroups = "weapons-groups",
|
||||
WeaponsThumbnails = "weapons-thumbnails",
|
||||
Folders = "folders",
|
||||
FoldersThumbnails = "FoldersThumbnails",
|
||||
FoldersThumbnails = "folders-thumbnails",
|
||||
Tags = "tags",
|
||||
TagsGroups = "tags-groups",
|
||||
Images = "images",
|
||||
}
|
||||
|
||||
export enum CollectionGroups {
|
||||
|
@ -95,3 +103,153 @@ export enum VideoSources {
|
|||
NicoNico = "NicoNico",
|
||||
Tumblr = "Tumblr",
|
||||
}
|
||||
|
||||
export enum PageType {
|
||||
Content = "Content",
|
||||
Article = "Article",
|
||||
Generic = "Generic",
|
||||
}
|
||||
|
||||
/* RICH TEXT */
|
||||
|
||||
export type RichTextContent = {
|
||||
root: {
|
||||
children: RichTextNode[];
|
||||
direction: ("ltr" | "rtl") | null;
|
||||
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
|
||||
indent: number;
|
||||
type: string;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
};
|
||||
|
||||
export type RichTextNode = {
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
|
||||
export interface RichTextNodeWithChildren extends RichTextNode {
|
||||
children: RichTextNode[];
|
||||
}
|
||||
|
||||
export interface RichTextParagraphNode extends RichTextNodeWithChildren {
|
||||
type: "paragraph";
|
||||
}
|
||||
|
||||
export interface RichTextListNode extends RichTextNode {
|
||||
type: "list";
|
||||
children: RichTextNodeWithChildren[];
|
||||
listType: string;
|
||||
}
|
||||
|
||||
export interface RichTextListNumberNode extends RichTextListNode {
|
||||
listType: "number";
|
||||
}
|
||||
|
||||
export interface RichTextListBulletNode extends RichTextListNode {
|
||||
listType: "bullet";
|
||||
}
|
||||
|
||||
export interface RichTextListCheckNode extends RichTextListNode {
|
||||
listType: "check";
|
||||
}
|
||||
|
||||
export interface RichTextTextNode extends RichTextNode {
|
||||
type: "text";
|
||||
format: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface RichTextLinkNode extends RichTextNodeWithChildren {
|
||||
type: "link";
|
||||
fields: {
|
||||
linkType: "internal" | "custom";
|
||||
};
|
||||
}
|
||||
|
||||
export interface RichTextLinkInternalNode extends RichTextLinkNode {
|
||||
fields: {
|
||||
linkType: "internal";
|
||||
newTab: boolean;
|
||||
doc: {
|
||||
relationTo: string;
|
||||
value: any;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface RichTextLinkCustomNode extends RichTextLinkNode {
|
||||
fields: {
|
||||
linkType: "custom";
|
||||
newTab: boolean;
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RichTextBlockNode extends RichTextNode {
|
||||
type: "block";
|
||||
fields: {
|
||||
blockType: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RichTextSectionBlock extends RichTextBlockNode {
|
||||
fields: SectionBlock & { anchorHash: string };
|
||||
}
|
||||
|
||||
export interface RichTextTranscriptBlock extends RichTextBlockNode {
|
||||
fields: TranscriptBlock;
|
||||
}
|
||||
|
||||
export const isNodeParagraphNode = (node: RichTextNode): node is RichTextParagraphNode =>
|
||||
node.type === "paragraph";
|
||||
|
||||
export const isNodeListNode = (node: RichTextNode): node is RichTextListNode =>
|
||||
node.type === "list";
|
||||
|
||||
export const isListNodeNumberListNode = (node: RichTextListNode): node is RichTextListNumberNode =>
|
||||
node.listType === "number";
|
||||
|
||||
export const isListNodeBulletListNode = (node: RichTextListNode): node is RichTextListBulletNode =>
|
||||
node.listType === "bullet";
|
||||
|
||||
export const isListNodeCheckListNode = (node: RichTextListNode): node is RichTextListCheckNode =>
|
||||
node.listType === "check";
|
||||
|
||||
export const isNodeTextNode = (node: RichTextNode): node is RichTextTextNode =>
|
||||
node.type === "text";
|
||||
|
||||
export const isNodeLinkNode = (node: RichTextNode): node is RichTextLinkNode =>
|
||||
node.type === "link";
|
||||
|
||||
export const isLinkNodeInternalLinkNode = (
|
||||
node: RichTextLinkNode
|
||||
): node is RichTextLinkInternalNode => node.fields.linkType === "internal";
|
||||
|
||||
export const isLinkNodeCustomLinkNode = (node: RichTextLinkNode): node is RichTextLinkCustomNode =>
|
||||
node.fields.linkType === "custom";
|
||||
|
||||
export const isNodeBlockNode = (node: RichTextNode): node is RichTextBlockNode =>
|
||||
node.type === "block";
|
||||
|
||||
export const isBlockNodeSectionBlock = (node: RichTextBlockNode): node is RichTextSectionBlock =>
|
||||
node.fields.blockType === "sectionBlock";
|
||||
|
||||
export const isBlockNodeTranscriptBlock = (
|
||||
node: RichTextBlockNode
|
||||
): node is RichTextTranscriptBlock => node.fields.blockType === "transcriptBlock";
|
||||
|
||||
/* BLOCKS */
|
||||
|
||||
export interface GenericBlock {
|
||||
content: unknown;
|
||||
blockType: string;
|
||||
}
|
||||
|
||||
export const isBlockCueBlock = (block: GenericBlock): block is CueBlock =>
|
||||
block.blockType === "cueBlock";
|
||||
|
||||
export const isBlockLineBlock = (block: GenericBlock): block is LineBlock =>
|
||||
block.blockType === "lineBlock";
|
||||
|
|
|
@ -5,4 +5,8 @@ type Props = Omit<TextField, "type" | "hasMany" | "maxRows" | "minRows">;
|
|||
export const iconField = (props: Props): TextField => ({
|
||||
...props,
|
||||
type: "text",
|
||||
admin: {
|
||||
description:
|
||||
"Select an icon from here: https://icones.js.org/collection/material-symbols. Only outline and regular variants are usable on the website.",
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { FieldBase, RelationshipField } from "payload/dist/fields/config/types";
|
||||
import { FieldBase, SingleRelationshipField } from "payload/dist/fields/config/types";
|
||||
import { Collections, KeysTypes } from "../../constants";
|
||||
|
||||
type KeysField = FieldBase & {
|
||||
relationTo: KeysTypes;
|
||||
hasMany?: boolean;
|
||||
admin?: RelationshipField["admin"];
|
||||
admin?: SingleRelationshipField["admin"];
|
||||
};
|
||||
|
||||
export const keysField = ({
|
||||
|
@ -12,7 +12,7 @@ export const keysField = ({
|
|||
hasMany = false,
|
||||
admin,
|
||||
...props
|
||||
}: KeysField): RelationshipField => ({
|
||||
}: KeysField): SingleRelationshipField => ({
|
||||
...props,
|
||||
admin: {
|
||||
allowCreate: false,
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { FieldBase, SingleRelationshipField } from "payload/dist/fields/config/types";
|
||||
import { Collections } from "../../constants";
|
||||
|
||||
type KeysField = FieldBase & {
|
||||
admin?: SingleRelationshipField["admin"];
|
||||
};
|
||||
|
||||
export const tagsField = ({ admin, ...props }: KeysField): SingleRelationshipField => ({
|
||||
...props,
|
||||
admin: {
|
||||
allowCreate: false,
|
||||
...admin,
|
||||
},
|
||||
type: "relationship",
|
||||
hasMany: true,
|
||||
relationTo: Collections.Tags,
|
||||
});
|
|
@ -5,12 +5,12 @@ import { buildConfig } from "payload/config";
|
|||
import { ChronologyEras } from "./collections/ChronologyEras/ChronologyEras";
|
||||
import { ChronologyItems } from "./collections/ChronologyItems/ChronologyItems";
|
||||
import { Contents } from "./collections/Contents/Contents";
|
||||
import { ContentsFolders } from "./collections/ContentsFolders/ContentsFolders";
|
||||
import { ContentsThumbnails } from "./collections/ContentsThumbnails/ContentsThumbnails";
|
||||
import { Currencies } from "./collections/Currencies/Currencies";
|
||||
import { Files } from "./collections/Files/Files";
|
||||
import { Folders } from "./collections/Folders/Folders";
|
||||
import { FoldersThumbnails } from "./collections/FoldersThumbnails/FoldersThumbnails";
|
||||
import { Images } from "./collections/Images/Images";
|
||||
import { Keys } from "./collections/Keys/Keys";
|
||||
import { Languages } from "./collections/Languages/Languages";
|
||||
import { LibraryItems } from "./collections/LibraryItems/LibraryItems";
|
||||
|
@ -18,10 +18,13 @@ import { LibraryItemsGallery } from "./collections/LibraryItemsGallery/LibraryIt
|
|||
import { LibraryItemsScans } from "./collections/LibraryItemsScans/LibraryItemsScans";
|
||||
import { LibraryItemsThumbnails } from "./collections/LibraryItemsThumbnails/LibraryItemsThumbnails";
|
||||
import { Notes } from "./collections/Notes/Notes";
|
||||
import { Pages } from "./collections/Pages/Pages";
|
||||
import { Posts } from "./collections/Posts/Posts";
|
||||
import { PostsThumbnails } from "./collections/PostsThumbnails/PostsThumbnails";
|
||||
import { Recorders } from "./collections/Recorders/Recorders";
|
||||
import { RecordersThumbnails } from "./collections/RecordersThumbnails/RecordersThumbnails";
|
||||
import { Tags } from "./collections/Tags/Tags";
|
||||
import { TagsGroups } from "./collections/TagsGroups/TagsGroups";
|
||||
import { Videos } from "./collections/Videos/Videos";
|
||||
import { VideosChannels } from "./collections/VideosChannels/VideosChannels";
|
||||
import { Weapons } from "./collections/Weapons/Weapons";
|
||||
|
@ -51,8 +54,8 @@ export default buildConfig({
|
|||
FoldersThumbnails,
|
||||
LibraryItems,
|
||||
Contents,
|
||||
ContentsFolders,
|
||||
Posts,
|
||||
Pages,
|
||||
ChronologyItems,
|
||||
ChronologyEras,
|
||||
Weapons,
|
||||
|
@ -72,6 +75,9 @@ export default buildConfig({
|
|||
Currencies,
|
||||
Recorders,
|
||||
Keys,
|
||||
Tags,
|
||||
TagsGroups,
|
||||
Images
|
||||
],
|
||||
db: mongooseAdapter({
|
||||
url: process.env.MONGODB_URI ?? "mongodb://mongo:27017/payload",
|
||||
|
|
152
src/sdk.ts
152
src/sdk.ts
|
@ -1,5 +1,5 @@
|
|||
import { Collections } from "./constants";
|
||||
import { Currency, Language } from "./types/collections";
|
||||
import { Collections, PageType, RichTextContent } from "./constants";
|
||||
import { Content, Currency, Key, Language, LibraryItem, Page } from "./types/collections";
|
||||
|
||||
class NodeCache {
|
||||
constructor(_params: any) {}
|
||||
|
@ -34,8 +34,8 @@ const refreshToken = async () => {
|
|||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
email: process.env.PAYLOAD_USER,
|
||||
password: process.env.PAYLOAD_PASSWORD,
|
||||
email: import.meta.env.PAYLOAD_USER,
|
||||
password: import.meta.env.PAYLOAD_PASSWORD,
|
||||
}),
|
||||
});
|
||||
logResponse(loginResult);
|
||||
|
@ -74,7 +74,7 @@ const injectAuth = async (init?: RequestInit): Promise<RequestInit> => ({
|
|||
const logResponse = (res: Response) => console.log(res.status, res.statusText, res.url);
|
||||
|
||||
const payloadApiUrl = (collection: Collections, endpoint?: string): string =>
|
||||
`${process.env.PAYLOAD_API_URL}/${collection}${endpoint === undefined ? "" : `/${endpoint}`}`;
|
||||
`${import.meta.env.PAYLOAD_API_URL}/${collection}${endpoint === undefined ? "" : `/${endpoint}`}`;
|
||||
|
||||
const request = async (url: string, init?: RequestInit): Promise<Response> => {
|
||||
const result = await fetch(url, await injectAuth(init));
|
||||
|
@ -159,14 +159,7 @@ export type EndpointEra = {
|
|||
}[];
|
||||
};
|
||||
|
||||
export type EndpointFolder = {
|
||||
slug: string;
|
||||
icon?: string;
|
||||
translations: {
|
||||
language: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
}[];
|
||||
export type EndpointFolder = EndpointFolderPreview & {
|
||||
sections:
|
||||
| { type: "single"; subfolders: EndpointFolderPreview[] }
|
||||
| {
|
||||
|
@ -176,8 +169,20 @@ export type EndpointFolder = {
|
|||
subfolders: EndpointFolderPreview[];
|
||||
}[];
|
||||
};
|
||||
lightThumbnail?: PayloadImage;
|
||||
darkThumbnail?: PayloadImage;
|
||||
files: (
|
||||
| {
|
||||
relationTo: "library-items";
|
||||
value: LibraryItem;
|
||||
}
|
||||
| {
|
||||
relationTo: "contents";
|
||||
value: Content;
|
||||
}
|
||||
| {
|
||||
relationTo: "pages";
|
||||
value: Page;
|
||||
}
|
||||
)[];
|
||||
};
|
||||
|
||||
export type EndpointFolderPreview = {
|
||||
|
@ -186,12 +191,115 @@ export type EndpointFolderPreview = {
|
|||
translations: {
|
||||
language: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
description?: RichTextContent;
|
||||
}[];
|
||||
lightThumbnail?: PayloadImage;
|
||||
darkThumbnail?: PayloadImage;
|
||||
};
|
||||
|
||||
export type EndpointContent = {
|
||||
slug: string;
|
||||
thumbnail?: PayloadImage;
|
||||
tagGroups: TagGroup[];
|
||||
translations: {
|
||||
language: string;
|
||||
sourceLanguage: string;
|
||||
pretitle?: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
summary?: RichTextContent;
|
||||
format: {
|
||||
text?: {
|
||||
content: RichTextContent;
|
||||
toc: TableOfContentEntry[];
|
||||
transcribers: string[];
|
||||
translators: string[];
|
||||
proofreaders: string[];
|
||||
notes?: RichTextContent;
|
||||
};
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
export type EndpointRecorder = {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar?: PayloadImage;
|
||||
languages: string[];
|
||||
biographies: {
|
||||
language: string;
|
||||
biography: RichTextContent;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type EndpointKey = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: Key["type"];
|
||||
translations: {
|
||||
language: string;
|
||||
name: string;
|
||||
short: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type EndpointTag = {
|
||||
slug: string;
|
||||
translations: {
|
||||
language: string;
|
||||
name: string;
|
||||
}[];
|
||||
group: string;
|
||||
};
|
||||
|
||||
export type EndpointTagsGroup = {
|
||||
slug: string;
|
||||
icon?: string;
|
||||
translations: {
|
||||
language: string;
|
||||
name: string;
|
||||
}[];
|
||||
tags: EndpointTag[];
|
||||
};
|
||||
|
||||
export type EndpointPage = {
|
||||
slug: string;
|
||||
type: PageType;
|
||||
thumbnail?: PayloadImage;
|
||||
authors: string[];
|
||||
tagGroups: TagGroup[];
|
||||
translations: {
|
||||
language: string;
|
||||
sourceLanguage: string;
|
||||
pretitle?: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
summary?: RichTextContent;
|
||||
content: RichTextContent;
|
||||
transcribers: string[];
|
||||
translators: string[];
|
||||
proofreaders: string[];
|
||||
toc: TableOfContentEntry[];
|
||||
}[];
|
||||
status: "draft" | "published";
|
||||
parentPages: ParentPage[];
|
||||
};
|
||||
|
||||
export type ParentPage = {
|
||||
slug: string;
|
||||
collection: Collections;
|
||||
translations: { language: string; name: string }[];
|
||||
tag: string;
|
||||
};
|
||||
|
||||
export type TagGroup = { slug: string; icon: string; values: string[] };
|
||||
|
||||
export type TableOfContentEntry = {
|
||||
prefix: string;
|
||||
title: string;
|
||||
children: TableOfContentEntry[];
|
||||
};
|
||||
|
||||
export type PayloadImage = {
|
||||
url: string;
|
||||
width: number;
|
||||
|
@ -213,4 +321,16 @@ export const payload = {
|
|||
await (await request(payloadApiUrl(Collections.Languages, `all`))).json(),
|
||||
getCurrencies: async (): Promise<Currency[]> =>
|
||||
await (await request(payloadApiUrl(Collections.Currencies, `all`))).json(),
|
||||
getContent: async (slug: string): Promise<EndpointContent> =>
|
||||
await (await request(payloadApiUrl(Collections.Contents, `slug/${slug}`))).json(),
|
||||
getKeys: async (): Promise<EndpointKey[]> =>
|
||||
await (await request(payloadApiUrl(Collections.Keys, `all`))).json(),
|
||||
getRecorders: async (): Promise<EndpointRecorder[]> =>
|
||||
await (await request(payloadApiUrl(Collections.Recorders, `all`))).json(),
|
||||
getTags: async (): Promise<EndpointTag[]> =>
|
||||
await (await request(payloadApiUrl(Collections.Tags, `all`))).json(),
|
||||
getTagsGroups: async (): Promise<EndpointTagsGroup[]> =>
|
||||
await (await request(payloadApiUrl(Collections.TagsGroups, `all`))).json(),
|
||||
getPage: async (slug: string): Promise<EndpointPage> =>
|
||||
await (await request(payloadApiUrl(Collections.Pages, `slug/${slug}`))).json(),
|
||||
};
|
||||
|
|
|
@ -59,19 +59,32 @@ const start = async () => {
|
|||
app.use("/public", express.static(path.join(__dirname, "../public")));
|
||||
|
||||
app.get("/api/sdk", (_, res) => {
|
||||
const collections = readFileSync(path.join(__dirname, "types/collections.ts"), "utf-8");
|
||||
const removeMockingSection = (text: string): string => {
|
||||
const lines = text.split("\n");
|
||||
const endMockingLine = lines.findIndex((line) => line === "// END MOCKING SECTION") ?? 0;
|
||||
return lines.slice(endMockingLine + 1).join("\n");
|
||||
};
|
||||
|
||||
const constantsHeader = "/////////////// CONSTANTS ///////////////\n";
|
||||
const constants = readFileSync(path.join(__dirname, "constants.ts"), "utf-8");
|
||||
const removeDeclare = (text: string): string => {
|
||||
const lines = text.split("\n");
|
||||
const startDeclareLine =
|
||||
lines.findIndex((line) => line === "declare module 'payload' {") ?? 0;
|
||||
return lines.slice(0, startDeclareLine).join("\n");
|
||||
};
|
||||
|
||||
const sdkHeader = "////////////////// SDK //////////////////\n";
|
||||
const sdkLines = readFileSync(path.join(__dirname, "sdk.ts"), "utf-8").split("\n");
|
||||
const endMockingLine = sdkLines.findIndex((line) => line === "// END MOCKING SECTION") ?? 0;
|
||||
const sdk =
|
||||
`import NodeCache from "node-cache";\n\n` + sdkLines.slice(endMockingLine + 1).join("\n");
|
||||
const result = [];
|
||||
|
||||
result.push(removeDeclare(readFileSync(path.join(__dirname, "types/collections.ts"), "utf-8")));
|
||||
|
||||
result.push("/////////////// CONSTANTS ///////////////");
|
||||
result.push(removeMockingSection(readFileSync(path.join(__dirname, "constants.ts"), "utf-8")));
|
||||
|
||||
result.push("////////////////// SDK //////////////////");
|
||||
result.push(`import NodeCache from "node-cache";`);
|
||||
result.push(removeMockingSection(readFileSync(path.join(__dirname, "sdk.ts"), "utf-8")));
|
||||
|
||||
res.type("text/plain");
|
||||
res.send([collections, constantsHeader, constants, sdkHeader, sdk].join("\n\n"));
|
||||
res.send(result.join("\n\n"));
|
||||
});
|
||||
|
||||
app.get("/robots.txt", (_, res) => {
|
||||
|
|
|
@ -6,6 +6,10 @@
|
|||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "RecorderBiographies".
|
||||
*/
|
||||
export type RecorderBiographies =
|
||||
| {
|
||||
language: string | Language;
|
||||
|
@ -27,6 +31,10 @@ export type RecorderBiographies =
|
|||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "CategoryTranslations".
|
||||
*/
|
||||
export type CategoryTranslations =
|
||||
| {
|
||||
language: string | Language;
|
||||
|
@ -39,11 +47,11 @@ export type CategoryTranslations =
|
|||
export interface Config {
|
||||
collections: {
|
||||
folders: Folder;
|
||||
FoldersThumbnails: FoldersThumbnail;
|
||||
'folders-thumbnails': FoldersThumbnail;
|
||||
'library-items': LibraryItem;
|
||||
contents: Content;
|
||||
'contents-folders': ContentsFolder;
|
||||
posts: Post;
|
||||
pages: Page;
|
||||
'chronology-items': ChronologyItem;
|
||||
'chronology-eras': ChronologyEra;
|
||||
weapons: Weapon;
|
||||
|
@ -56,18 +64,25 @@ export interface Config {
|
|||
'recorders-thumbnails': RecordersThumbnail;
|
||||
'posts-thumbnails': PostThumbnail;
|
||||
files: File;
|
||||
Notes: Note;
|
||||
notes: Note;
|
||||
videos: Video;
|
||||
'videos-channels': VideosChannel;
|
||||
languages: Language;
|
||||
currencies: Currency;
|
||||
recorders: Recorder;
|
||||
keys: Key;
|
||||
tags: Tag;
|
||||
'tags-groups': TagsGroup;
|
||||
images: Image;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
globals: {};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "folders".
|
||||
*/
|
||||
export interface Folder {
|
||||
id: string;
|
||||
slug: string;
|
||||
|
@ -119,11 +134,19 @@ export interface Folder {
|
|||
relationTo: 'contents';
|
||||
value: string | Content;
|
||||
}
|
||||
| {
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
}
|
||||
)[]
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "folders-thumbnails".
|
||||
*/
|
||||
export interface FoldersThumbnail {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
|
@ -143,20 +166,20 @@ export interface FoldersThumbnail {
|
|||
filesize?: number | null;
|
||||
filename?: string | null;
|
||||
};
|
||||
medium?: {
|
||||
url?: string | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
filename?: string | null;
|
||||
};
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "languages".
|
||||
*/
|
||||
export interface Language {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "library-items".
|
||||
*/
|
||||
export interface LibraryItem {
|
||||
id: string;
|
||||
itemType?: ('Textual' | 'Audio' | 'Video' | 'Game' | 'Other') | null;
|
||||
|
@ -323,6 +346,10 @@ export interface LibraryItem {
|
|||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "library-items-thumbnails".
|
||||
*/
|
||||
export interface LibraryItemThumbnail {
|
||||
id: string;
|
||||
libraryItem?: (string | LibraryItem)[] | null;
|
||||
|
@ -361,6 +388,10 @@ export interface LibraryItemThumbnail {
|
|||
};
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "library-items-gallery".
|
||||
*/
|
||||
export interface LibraryItemGallery {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
|
@ -390,6 +421,10 @@ export interface LibraryItemGallery {
|
|||
};
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "recorders".
|
||||
*/
|
||||
export interface Recorder {
|
||||
id: string;
|
||||
username: string;
|
||||
|
@ -407,6 +442,10 @@ export interface Recorder {
|
|||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "recorders-thumbnails".
|
||||
*/
|
||||
export interface RecordersThumbnail {
|
||||
id: string;
|
||||
recorder?: (string | null) | Recorder;
|
||||
|
@ -437,6 +476,10 @@ export interface RecordersThumbnail {
|
|||
};
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "library-items-scans".
|
||||
*/
|
||||
export interface LibraryItemScans {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
|
@ -482,6 +525,10 @@ export interface LibraryItemScans {
|
|||
};
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "files".
|
||||
*/
|
||||
export interface File {
|
||||
id: string;
|
||||
filename: string;
|
||||
|
@ -489,6 +536,10 @@ export interface File {
|
|||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "keys".
|
||||
*/
|
||||
export interface Key {
|
||||
id: string;
|
||||
name: string;
|
||||
|
@ -505,16 +556,22 @@ export interface Key {
|
|||
| 'Wordings';
|
||||
translations?: CategoryTranslations;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "currencies".
|
||||
*/
|
||||
export interface Currency {
|
||||
id: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "contents".
|
||||
*/
|
||||
export interface Content {
|
||||
id: string;
|
||||
slug: string;
|
||||
thumbnail?: string | ContentsThumbnail | null;
|
||||
categories?: (string | Key)[] | null;
|
||||
type?: (string | null) | Key;
|
||||
libraryItems?: (string | LibraryItem)[] | null;
|
||||
tags?: (string | Tag)[] | null;
|
||||
translations: {
|
||||
language: string | Language;
|
||||
sourceLanguage: string | Language;
|
||||
|
@ -603,7 +660,8 @@ export interface Content {
|
|||
} | null;
|
||||
id?: string | null;
|
||||
}[];
|
||||
folders?: (string | ContentsFolder)[] | null;
|
||||
folders?: (string | Folder)[] | null;
|
||||
libraryItems?: (string | LibraryItem)[] | null;
|
||||
previousContents?: (string | Content)[] | null;
|
||||
nextContents?: (string | Content)[] | null;
|
||||
updatedBy: string | Recorder;
|
||||
|
@ -611,6 +669,10 @@ export interface Content {
|
|||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "contents-thumbnails".
|
||||
*/
|
||||
export interface ContentsThumbnail {
|
||||
id: string;
|
||||
contents?: (string | Content)[] | null;
|
||||
|
@ -649,8 +711,13 @@ export interface ContentsThumbnail {
|
|||
};
|
||||
};
|
||||
}
|
||||
export interface ContentsFolder {
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "tags".
|
||||
*/
|
||||
export interface Tag {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
slug: string;
|
||||
translations?:
|
||||
| {
|
||||
|
@ -659,9 +726,124 @@ export interface ContentsFolder {
|
|||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
subfolders?: (string | ContentsFolder)[] | null;
|
||||
contents?: (string | Content)[] | null;
|
||||
group: string | TagsGroup;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "tags-groups".
|
||||
*/
|
||||
export interface TagsGroup {
|
||||
id: string;
|
||||
slug: string;
|
||||
icon?: string | null;
|
||||
translations?:
|
||||
| {
|
||||
language: string | Language;
|
||||
name: string;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages".
|
||||
*/
|
||||
export interface Page {
|
||||
id: string;
|
||||
type: 'Content' | 'Article' | 'Generic';
|
||||
slug: string;
|
||||
thumbnail?: string | Image | null;
|
||||
tags?: (string | Tag)[] | null;
|
||||
authors?: (string | Recorder)[] | null;
|
||||
translations: {
|
||||
language: string | Language;
|
||||
sourceLanguage: string | Language;
|
||||
pretitle?: string | null;
|
||||
title: string;
|
||||
subtitle?: string | null;
|
||||
summary?: {
|
||||
root: {
|
||||
children: {
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
type: string;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
content: {
|
||||
root: {
|
||||
children: {
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
type: string;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
};
|
||||
transcribers?: (string | Recorder)[] | null;
|
||||
translators?: (string | Recorder)[] | null;
|
||||
proofreaders?: (string | Recorder)[] | null;
|
||||
id?: string | null;
|
||||
}[];
|
||||
folders?: (string | Folder)[] | null;
|
||||
collectibles?: (string | LibraryItem)[] | null;
|
||||
updatedBy: string | Recorder;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "images".
|
||||
*/
|
||||
export interface Image {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
sizes?: {
|
||||
thumb?: {
|
||||
url?: string | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
filename?: string | null;
|
||||
};
|
||||
og?: {
|
||||
url?: string | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
filename?: string | null;
|
||||
};
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts".
|
||||
*/
|
||||
export interface Post {
|
||||
id: string;
|
||||
slug: string;
|
||||
|
@ -713,6 +895,10 @@ export interface Post {
|
|||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts-thumbnails".
|
||||
*/
|
||||
export interface PostThumbnail {
|
||||
id: string;
|
||||
posts?: (string | Post)[] | null;
|
||||
|
@ -751,6 +937,10 @@ export interface PostThumbnail {
|
|||
};
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "chronology-items".
|
||||
*/
|
||||
export interface ChronologyItem {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
|
@ -806,6 +996,10 @@ export interface ChronologyItem {
|
|||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "chronology-eras".
|
||||
*/
|
||||
export interface ChronologyEra {
|
||||
id: string;
|
||||
slug: string;
|
||||
|
@ -837,6 +1031,10 @@ export interface ChronologyEra {
|
|||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "weapons".
|
||||
*/
|
||||
export interface Weapon {
|
||||
id: string;
|
||||
slug: string;
|
||||
|
@ -936,6 +1134,10 @@ export interface Weapon {
|
|||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "weapons-thumbnails".
|
||||
*/
|
||||
export interface WeaponsThumbnail {
|
||||
id: string;
|
||||
weapon?: (string | null) | Weapon;
|
||||
|
@ -982,6 +1184,10 @@ export interface WeaponsThumbnail {
|
|||
};
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "weapons-groups".
|
||||
*/
|
||||
export interface WeaponsGroup {
|
||||
id: string;
|
||||
slug: string;
|
||||
|
@ -994,6 +1200,10 @@ export interface WeaponsGroup {
|
|||
| null;
|
||||
weapons?: (string | Weapon)[] | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "notes".
|
||||
*/
|
||||
export interface Note {
|
||||
id: string;
|
||||
note: {
|
||||
|
@ -1014,6 +1224,10 @@ export interface Note {
|
|||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "videos".
|
||||
*/
|
||||
export interface Video {
|
||||
id: string;
|
||||
uid: string;
|
||||
|
@ -1026,12 +1240,20 @@ export interface Video {
|
|||
publishedDate: string;
|
||||
channel?: (string | null) | VideosChannel;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "videos-channels".
|
||||
*/
|
||||
export interface VideosChannel {
|
||||
id: string;
|
||||
uid: string;
|
||||
title: string;
|
||||
subscribers?: number | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
user: {
|
||||
|
@ -1051,6 +1273,10 @@ export interface PayloadPreference {
|
|||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
|
@ -1058,6 +1284,84 @@ export interface PayloadMigration {
|
|||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "LineBlock".
|
||||
*/
|
||||
export interface LineBlock {
|
||||
content: {
|
||||
root: {
|
||||
children: {
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
type: string;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
};
|
||||
blockType: 'lineBlock';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "CueBlock".
|
||||
*/
|
||||
export interface CueBlock {
|
||||
content: {
|
||||
root: {
|
||||
children: {
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
type: string;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
};
|
||||
blockType: 'cueBlock';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "TranscriptBlock".
|
||||
*/
|
||||
export interface TranscriptBlock {
|
||||
lines: (LineBlock | CueBlock)[];
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'transcriptBlock';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "SectionBlock".
|
||||
*/
|
||||
export interface SectionBlock {
|
||||
lines: {
|
||||
root: {
|
||||
children: {
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
type: string;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
};
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'sectionBlock';
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
|
|
|
@ -32,9 +32,12 @@ export const isValidPayloadImage = (
|
|||
height?: number | null;
|
||||
url?: string | null;
|
||||
}
|
||||
| undefined | null
|
||||
| undefined
|
||||
| null
|
||||
| string
|
||||
): image is PayloadImage => {
|
||||
if (isUndefined(image)) return false;
|
||||
if (typeof image === "string") return false;
|
||||
if (isEmpty(image.filename)) return false;
|
||||
if (isEmpty(image.url)) return false;
|
||||
if (isEmpty(image.mimeType)) return false;
|
||||
|
@ -49,5 +52,6 @@ export const isString = <T extends Object>(value: string | T): value is string =
|
|||
export const isPayloadType = <T extends Object>(value: string | T): value is T =>
|
||||
typeof value === "object";
|
||||
|
||||
export const isPayloadArrayType = <T extends Object>(value: string[] | T[]): value is T[] =>
|
||||
value.every(isPayloadType<T>);
|
||||
export const isPayloadArrayType = <T extends Object>(
|
||||
value: (string | T)[] | null | undefined
|
||||
): value is T[] => isDefined(value) && value.every(isPayloadType<T>);
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { TagGroup } from "../sdk";
|
||||
import { Tag } from "../types/collections";
|
||||
import { isPayloadArrayType, isPayloadType } from "./asserts";
|
||||
|
||||
export const convertTagsToGroups = (tags: (string | Tag)[] | null | undefined): TagGroup[] => {
|
||||
if (!isPayloadArrayType(tags)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const groups: TagGroup[] = [];
|
||||
|
||||
tags.forEach(({ group, slug }) => {
|
||||
if (isPayloadType(group)) {
|
||||
const existingGroup = groups.find((existingGroup) => existingGroup.slug === group.slug);
|
||||
if (existingGroup) {
|
||||
existingGroup.values.push(slug);
|
||||
} else {
|
||||
groups.push({
|
||||
slug: group.slug,
|
||||
icon: group.icon ?? "material-symbols:category-outline",
|
||||
values: [slug],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
Loading…
Reference in New Issue