Lots of things, again, again
This commit is contained in:
parent
f3253ce972
commit
6074ccceff
|
@ -0,0 +1,9 @@
|
|||
import { RecordersRoles } from "../../constants";
|
||||
import { Recorder } from "../../types/collections";
|
||||
import { EndpointAccess } from "../../types/payload";
|
||||
import { isDefined, isUndefined } from "../../utils/asserts";
|
||||
|
||||
export const mustBeAdmin: EndpointAccess<Recorder> = ({ user }) => {
|
||||
if (isUndefined(user)) return false;
|
||||
return isDefined(user.role) && user.role.includes(RecordersRoles.Admin);
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
import { RecordersRoles } from "../../constants";
|
||||
import { Recorder } from "../../types/collections";
|
||||
import { EndpointAccess } from "../../types/payload";
|
||||
import { isDefined, isUndefined } from "../../utils/asserts";
|
||||
|
||||
export const mustBeApi: EndpointAccess<Recorder> = ({ user }) => {
|
||||
if (isUndefined(user)) return false;
|
||||
return isDefined(user.role) && user.role.includes(RecordersRoles.Api);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
import { Recorder } from "../../types/collections";
|
||||
import { EndpointAccess } from "../../types/payload";
|
||||
import { isDefined, isUndefined } from "../../utils/asserts";
|
||||
|
||||
export const mustHaveAtLeastOneRole: EndpointAccess<Recorder> = ({ user }) => {
|
||||
if (isUndefined(user)) return false;
|
||||
return isDefined(user.role) && user.role.length > 0;
|
||||
};
|
|
@ -1,4 +1,3 @@
|
|||
import { DateTime } from "luxon";
|
||||
import { CollectionConfig } from "payload/types";
|
||||
import {
|
||||
QuickFilters,
|
||||
|
@ -7,10 +6,12 @@ import {
|
|||
} from "../../components/QuickFilters";
|
||||
import { CollectionGroups, Collections } from "../../constants";
|
||||
import { translatedFields } from "../../fields/translatedFields/translatedFields";
|
||||
import { isEmpty, isUndefined } from "../../utils/asserts";
|
||||
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig";
|
||||
import { importFromStrapi } from "./endpoints/importFromStrapi";
|
||||
import { beforeValidatePopulateNameField } from "./hooks/beforeValidatePopulateNameField";
|
||||
import { validateDate } from "./validations/validateDate";
|
||||
import { validateEventsTranslationsDescription } from "./validations/validateEventsTranslationsDescription";
|
||||
import { validateEventsTranslationsTitle } from "./validations/validateEventsTranslationsTitle";
|
||||
|
||||
const fields = {
|
||||
name: "name",
|
||||
|
@ -65,15 +66,7 @@ export const ChronologyItems: CollectionConfig = buildVersionedCollectionConfig(
|
|||
{
|
||||
type: "group",
|
||||
name: fields.date,
|
||||
validate: ({ year, month, day } = {}) => {
|
||||
if (isUndefined(day)) return true;
|
||||
if (isUndefined(month)) return "A month is required if a day is set";
|
||||
const stringDate = `${year}/${month}/${day}`;
|
||||
if (!DateTime.fromObject({ year, month, day }).isValid) {
|
||||
return `The given date (${stringDate}) is not a valid date.`;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
validate: validateDate,
|
||||
fields: [
|
||||
{
|
||||
type: "row",
|
||||
|
@ -116,22 +109,12 @@ export const ChronologyItems: CollectionConfig = buildVersionedCollectionConfig(
|
|||
fields: [
|
||||
{
|
||||
name: fields.eventsTranslationsTitle,
|
||||
validate: (_, { siblingData: { description, title } }) => {
|
||||
if (isEmpty(description) && isEmpty(title)) {
|
||||
return "This field is required if no description is set.";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
validate: validateEventsTranslationsTitle,
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
name: fields.eventsTranslationsDescription,
|
||||
validate: (_, { siblingData: { description, title } }) => {
|
||||
if (isEmpty(description) && isEmpty(title)) {
|
||||
return "This field is required if no title is set.";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
validate: validateEventsTranslationsDescription,
|
||||
type: "textarea",
|
||||
},
|
||||
{ name: fields.eventsTranslationsNotes, type: "textarea" },
|
||||
|
|
|
@ -7,17 +7,14 @@ export const beforeValidatePopulateNameField: FieldHook<
|
|||
ChronologyItem["name"],
|
||||
ChronologyItem
|
||||
> = ({ data }) => {
|
||||
if (isUndefined(data)) {
|
||||
if (isUndefined(data) || isUndefined(data.date) || isUndefined(data.date.year))
|
||||
return "????-??-??";
|
||||
}
|
||||
const { date } = data;
|
||||
if (isUndefined(date) || isUndefined(date?.year)) return "????-??-??";
|
||||
const { year, month, day } = date;
|
||||
const { year, month, day } = data.date;
|
||||
let result = String(year).padStart(5, " ");
|
||||
if (isDefined(month)) {
|
||||
result += `-${String(date.month).padStart(2, "0")}`;
|
||||
result += `-${String(month).padStart(2, "0")}`;
|
||||
if (isDefined(day)) {
|
||||
result += `-${String(date.day).padStart(2, "0")}`;
|
||||
result += `-${String(day).padStart(2, "0")}`;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import { DateTime } from "luxon";
|
||||
import { Validate } from "payload/types";
|
||||
import { ChronologyItem } from "../../../types/collections";
|
||||
import { isUndefined } from "../../../utils/asserts";
|
||||
|
||||
export const validateDate: Validate<ChronologyItem["date"] | undefined> = (date) => {
|
||||
if (isUndefined(date)) return "This field is required.";
|
||||
const { year, month, day } = date;
|
||||
if (isUndefined(day)) return true;
|
||||
if (isUndefined(month)) return "A month is required if a day is set.";
|
||||
const stringDate = `${year}/${month}/${day}`;
|
||||
if (!DateTime.fromObject({ year, month, day }).isValid) {
|
||||
return `The given date (${stringDate}) is not a valid date.`;
|
||||
}
|
||||
return true;
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
import { Validate } from "payload/types";
|
||||
import { ChronologyItem } from "../../../types/collections";
|
||||
import { isEmpty } from "../../../utils/asserts";
|
||||
|
||||
export const validateEventsTranslationsDescription: Validate<
|
||||
string | undefined,
|
||||
ChronologyItem,
|
||||
ChronologyItem["events"][number]["translations"][number],
|
||||
unknown
|
||||
> = (_, { siblingData: { description, title } }) => {
|
||||
if (isEmpty(description) && isEmpty(title)) {
|
||||
return "This field is required if no title is set.";
|
||||
}
|
||||
return true;
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
import { Validate } from "payload/types";
|
||||
import { ChronologyItem } from "../../../types/collections";
|
||||
import { isEmpty } from "../../../utils/asserts";
|
||||
|
||||
export const validateEventsTranslationsTitle: Validate<
|
||||
string | undefined,
|
||||
ChronologyItem,
|
||||
ChronologyItem["events"][number]["translations"][number],
|
||||
unknown
|
||||
> = (_, { siblingData: { description, title } }) => {
|
||||
if (isEmpty(description) && isEmpty(title)) {
|
||||
return "This field is required if no description is set.";
|
||||
}
|
||||
return true;
|
||||
};
|
|
@ -46,8 +46,8 @@ export const Contents = buildVersionedCollectionConfig({
|
|||
description:
|
||||
"All the contents (textual, audio, and video) from the Library or other online sources.",
|
||||
defaultColumns: [
|
||||
fields.slug,
|
||||
fields.thumbnail,
|
||||
fields.slug,
|
||||
fields.categories,
|
||||
fields.type,
|
||||
fields.translations,
|
||||
|
@ -175,7 +175,7 @@ export const Contents = buildVersionedCollectionConfig({
|
|||
fields: [
|
||||
fileField({
|
||||
name: fields.video,
|
||||
filterOptions: { type: { equals: FileTypes.ContentVideo } },
|
||||
relationTo: FileTypes.ContentVideo,
|
||||
admin: { width: "50%" },
|
||||
}),
|
||||
{
|
||||
|
@ -196,7 +196,7 @@ export const Contents = buildVersionedCollectionConfig({
|
|||
fields: [
|
||||
fileField({
|
||||
name: fields.audio,
|
||||
filterOptions: { type: { equals: FileTypes.ContentAudio } },
|
||||
relationTo: FileTypes.ContentAudio,
|
||||
admin: { width: "50%" },
|
||||
}),
|
||||
{
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { CollectionGroups, Collections } from "../../constants";
|
||||
import { Collections } from "../../constants";
|
||||
import { backPropagationField } from "../../fields/backPropagationField/backPropagationField";
|
||||
import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig";
|
||||
|
||||
const fields = {
|
||||
filename: "filename",
|
||||
mimeType: "mimeType",
|
||||
filesize: "filesize",
|
||||
contents: "contents",
|
||||
updatedAt: "updatedAt",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
export const ContentsThumbnails = buildImageCollectionConfig({
|
||||
|
@ -13,12 +16,7 @@ export const ContentsThumbnails = buildImageCollectionConfig({
|
|||
singular: "Contents Thumbnail",
|
||||
plural: "Contents Thumbnails",
|
||||
},
|
||||
defaultSort: fields.filename,
|
||||
admin: {
|
||||
useAsTitle: fields.filename,
|
||||
disableDuplicate: true,
|
||||
group: CollectionGroups.Media,
|
||||
},
|
||||
admin: { defaultColumns: [fields.filename, fields.contents, fields.updatedAt] },
|
||||
upload: {
|
||||
imageSizes: [
|
||||
{
|
||||
|
@ -39,7 +37,21 @@ export const ContentsThumbnails = buildImageCollectionConfig({
|
|||
options: { effort: 6, quality: 80, alphaQuality: 80 },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "max",
|
||||
formatOptions: {
|
||||
format: "webp",
|
||||
options: { effort: 6, quality: 80, alphaQuality: 80 },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
fields: [],
|
||||
fields: [
|
||||
backPropagationField({
|
||||
name: fields.contents,
|
||||
hasMany: true,
|
||||
relationTo: Collections.Contents,
|
||||
where: ({ id }) => ({ thumbnail: { equals: id } }),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import { CollectionGroups, Collections, FileTypes } from "../../constants";
|
||||
import { File } from "../../types/collections";
|
||||
import { buildCollectionConfig } from "../../utils/collectionConfig";
|
||||
import {
|
||||
beforeValidateCheckFileExists,
|
||||
generatePathForFile,
|
||||
} from "./hooks/beforeValidateCheckFileExists";
|
||||
|
||||
const fields = {
|
||||
filename: "filename",
|
||||
|
@ -17,6 +22,13 @@ export const Files = buildCollectionConfig({
|
|||
useAsTitle: fields.filename,
|
||||
disableDuplicate: true,
|
||||
group: CollectionGroups.Media,
|
||||
preview: (doc) => {
|
||||
const { filename, type } = doc as unknown as File;
|
||||
return generatePathForFile(type, filename);
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
beforeValidate: [beforeValidateCheckFileExists],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import { CollectionBeforeValidateHook } from "payload/types";
|
||||
import { FileTypes } from "../../../constants";
|
||||
import { File } from "../../../types/collections";
|
||||
import { isUndefined } from "../../../utils/asserts";
|
||||
|
||||
const reshareSubFolderFromType: Record<keyof typeof FileTypes, string> = {
|
||||
ContentAudio: "/contents/audios",
|
||||
ContentVideo: "/contents/videos",
|
||||
LibraryScans: "/library/scans",
|
||||
LibrarySoundtracks: "/library/tracks",
|
||||
};
|
||||
|
||||
const expectedMimeFromType = {
|
||||
ContentAudio: "audio/",
|
||||
ContentVideo: "video/",
|
||||
LibraryScans: "application/zip",
|
||||
LibrarySoundtracks: "audio/",
|
||||
};
|
||||
|
||||
export const generatePathForFile = (type: keyof typeof FileTypes, filename: string) =>
|
||||
`https://resha.re/accords${reshareSubFolderFromType[type]}/${filename}`;
|
||||
|
||||
export const beforeValidateCheckFileExists: CollectionBeforeValidateHook<File> = async ({
|
||||
data,
|
||||
}) => {
|
||||
if (isUndefined(data)) throw new Error("The data is undefined");
|
||||
const { type, filename } = data;
|
||||
if (isUndefined(filename)) throw new Error("Filename is undefined");
|
||||
if (isUndefined(type)) throw new Error("Filename is undefined");
|
||||
|
||||
const url = generatePathForFile(type, filename);
|
||||
|
||||
const result = await fetch(url, { method: "HEAD" });
|
||||
|
||||
if (result.status !== 200) {
|
||||
throw new Error(`Unable to locate the file at the following address: ${url}`);
|
||||
}
|
||||
|
||||
const contentType = result.headers.get("content-type");
|
||||
if (isUndefined(contentType) || !contentType.startsWith(expectedMimeFromType[type])) {
|
||||
throw new Error(
|
||||
`Wrong MIME type found: ${contentType}. The expected MIME type was ${expectedMimeFromType[type]}`
|
||||
);
|
||||
}
|
||||
};
|
|
@ -2,11 +2,13 @@ import { RowLabelArgs } from "payload/dist/admin/components/forms/RowLabel/types
|
|||
import {
|
||||
CollectionGroups,
|
||||
Collections,
|
||||
FileTypes,
|
||||
KeysTypes,
|
||||
LibraryItemsTextualBindingTypes,
|
||||
LibraryItemsTextualPageOrders,
|
||||
LibraryItemsTypes,
|
||||
} from "../../constants";
|
||||
import { fileField } from "../../fields/fileField/fileField";
|
||||
import { imageField } from "../../fields/imageField/imageField";
|
||||
import { keysField } from "../../fields/keysField/keysField";
|
||||
import { optionalGroupField } from "../../fields/optionalGroupField/optionalGroupField";
|
||||
|
@ -66,6 +68,9 @@ const fields = {
|
|||
|
||||
audio: "audio",
|
||||
audioSubtype: "audioSubtype",
|
||||
audioTracks: "tracks",
|
||||
audioTracksFile: "file",
|
||||
audioTracksTitle: "title",
|
||||
|
||||
scans: "scans",
|
||||
|
||||
|
@ -129,7 +134,7 @@ export const LibraryItems = buildVersionedCollectionConfig({
|
|||
description:
|
||||
"A comprehensive list of all Yokoverse’s side materials (books, novellas, artbooks, \
|
||||
stage plays, manga, drama CDs, and comics).",
|
||||
defaultColumns: [fields.slug, fields.thumbnail, fields.status],
|
||||
defaultColumns: [fields.thumbnail, fields.slug, fields.status],
|
||||
group: CollectionGroups.Collections,
|
||||
hooks: {
|
||||
beforeDuplicate: beforeDuplicatePiping([
|
||||
|
@ -596,6 +601,29 @@ export const LibraryItems = buildVersionedCollectionConfig({
|
|||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: fields.audioTracks,
|
||||
type: "array",
|
||||
fields: [
|
||||
{
|
||||
type: "row",
|
||||
fields: [
|
||||
{
|
||||
name: fields.audioTracksTitle,
|
||||
type: "text",
|
||||
required: true,
|
||||
admin: { width: "50%" },
|
||||
},
|
||||
fileField({
|
||||
name: fields.audioTracksFile,
|
||||
relationTo: FileTypes.LibrarySoundtracks,
|
||||
required: true,
|
||||
admin: { width: "50%" },
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from "react";
|
||||
import { styled } from "styled-components";
|
||||
import { isDefined } from "../../../utils/asserts";
|
||||
import { formatLanguageCode, shortenEllipsis } from "../../../utils/string";
|
||||
|
||||
interface Props {
|
||||
page?: number;
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { CollectionGroups, Collections } from "../../constants";
|
||||
import { Collections } from "../../constants";
|
||||
import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig";
|
||||
|
||||
const fields = {
|
||||
filename: "filename",
|
||||
mimeType: "mimeType",
|
||||
filesize: "filesize",
|
||||
updatedAt: "updatedAt",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
export const LibraryItemsGallery = buildImageCollectionConfig({
|
||||
|
@ -13,12 +14,7 @@ export const LibraryItemsGallery = buildImageCollectionConfig({
|
|||
singular: "Library Item Gallery",
|
||||
plural: "Library Item Gallery",
|
||||
},
|
||||
defaultSort: fields.filename,
|
||||
admin: {
|
||||
useAsTitle: fields.filename,
|
||||
disableDuplicate: true,
|
||||
group: CollectionGroups.Media,
|
||||
},
|
||||
admin: { defaultColumns: [fields.filename, fields.updatedAt] },
|
||||
upload: {
|
||||
imageSizes: [
|
||||
{
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { CollectionGroups, Collections } from "../../constants";
|
||||
import { Collections } from "../../constants";
|
||||
import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig";
|
||||
|
||||
const fields = {
|
||||
filename: "filename",
|
||||
mimeType: "mimeType",
|
||||
filesize: "filesize",
|
||||
updatedAt: "updatedAt",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
export const LibraryItemsScans = buildImageCollectionConfig({
|
||||
|
@ -13,12 +14,7 @@ export const LibraryItemsScans = buildImageCollectionConfig({
|
|||
singular: "Library Item Scans",
|
||||
plural: "Library Item Scans",
|
||||
},
|
||||
defaultSort: fields.filename,
|
||||
admin: {
|
||||
useAsTitle: fields.filename,
|
||||
disableDuplicate: true,
|
||||
group: CollectionGroups.Media,
|
||||
},
|
||||
admin: { defaultColumns: [fields.filename, fields.updatedAt] },
|
||||
upload: {
|
||||
imageSizes: [
|
||||
{
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { CollectionGroups, Collections } from "../../constants";
|
||||
import { Collections } from "../../constants";
|
||||
import { backPropagationField } from "../../fields/backPropagationField/backPropagationField";
|
||||
import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig";
|
||||
|
||||
const fields = {
|
||||
filename: "filename",
|
||||
mimeType: "mimeType",
|
||||
filesize: "filesize",
|
||||
libraryItem: "libraryItem",
|
||||
updatedAt: "updatedAt",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
export const LibraryItemsThumbnails = buildImageCollectionConfig({
|
||||
|
@ -13,12 +16,7 @@ export const LibraryItemsThumbnails = buildImageCollectionConfig({
|
|||
singular: "Library Item Thumbnail",
|
||||
plural: "Library Item Thumbnails",
|
||||
},
|
||||
defaultSort: fields.filename,
|
||||
admin: {
|
||||
useAsTitle: fields.filename,
|
||||
disableDuplicate: true,
|
||||
group: CollectionGroups.Media,
|
||||
},
|
||||
admin: { defaultColumns: [fields.filename, fields.libraryItem, fields.updatedAt] },
|
||||
upload: {
|
||||
imageSizes: [
|
||||
{
|
||||
|
@ -51,5 +49,12 @@ export const LibraryItemsThumbnails = buildImageCollectionConfig({
|
|||
},
|
||||
],
|
||||
},
|
||||
fields: [],
|
||||
fields: [
|
||||
backPropagationField({
|
||||
name: fields.libraryItem,
|
||||
hasMany: true,
|
||||
relationTo: Collections.LibraryItems,
|
||||
where: ({ id }) => ({ thumbnail: { equals: id } }),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
|
@ -37,7 +37,7 @@ export const Posts = buildVersionedCollectionConfig({
|
|||
description:
|
||||
"News articles written by our Recorders! Here you will find announcements about \
|
||||
new merch/items releases, guides, theories, unboxings, showcases...",
|
||||
defaultColumns: [fields.slug, fields.thumbnail, fields.categories],
|
||||
defaultColumns: [fields.thumbnail, fields.slug, fields.categories],
|
||||
group: CollectionGroups.Collections,
|
||||
components: {
|
||||
BeforeListTable: [
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { CollectionGroups, Collections } from "../../constants";
|
||||
import { Collections } from "../../constants";
|
||||
import { backPropagationField } from "../../fields/backPropagationField/backPropagationField";
|
||||
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 PostsThumbnails = buildImageCollectionConfig({
|
||||
|
@ -13,12 +16,7 @@ export const PostsThumbnails = buildImageCollectionConfig({
|
|||
singular: "Post Thumbnail",
|
||||
plural: "Post Thumbnails",
|
||||
},
|
||||
defaultSort: fields.filename,
|
||||
admin: {
|
||||
useAsTitle: fields.filename,
|
||||
disableDuplicate: true,
|
||||
group: CollectionGroups.Media,
|
||||
},
|
||||
admin: { defaultColumns: [fields.filename, fields.posts, fields.updatedAt] },
|
||||
upload: {
|
||||
imageSizes: [
|
||||
{
|
||||
|
@ -41,5 +39,12 @@ export const PostsThumbnails = buildImageCollectionConfig({
|
|||
},
|
||||
],
|
||||
},
|
||||
fields: [],
|
||||
fields: [
|
||||
backPropagationField({
|
||||
name: fields.posts,
|
||||
hasMany: true,
|
||||
relationTo: Collections.Posts,
|
||||
where: ({ id }) => ({ thumbnail: { equals: id } }),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
|
@ -32,8 +32,8 @@ export const Recorders = buildCollectionConfig({
|
|||
"Recorders are contributors of the Accord's Library project. Ask an admin to create a \
|
||||
Recorder here to be able to credit them in other collections.",
|
||||
defaultColumns: [
|
||||
fields.username,
|
||||
fields.avatar,
|
||||
fields.username,
|
||||
fields.anonymize,
|
||||
fields.biographies,
|
||||
fields.languages,
|
||||
|
|
|
@ -4,7 +4,7 @@ import { createStrapiImportEndpoint } from "../../../endpoints/createStrapiImpor
|
|||
import { Recorder } from "../../../types/collections";
|
||||
import { PayloadCreateData } from "../../../types/payload";
|
||||
import { StrapiImage, StrapiLanguage } from "../../../types/strapi";
|
||||
import { isUndefined } from "../../../utils/asserts";
|
||||
import { isDefined, isUndefined } from "../../../utils/asserts";
|
||||
import { uploadStrapiImage } from "../../../utils/localApi";
|
||||
|
||||
type StrapiRecorder = {
|
||||
|
@ -31,9 +31,7 @@ export const importFromStrapi = createStrapiImportEndpoint<Recorder, StrapiRecor
|
|||
image: avatar,
|
||||
});
|
||||
|
||||
const data: PayloadCreateData<Recorder> = {
|
||||
email: `${anonymous_code}@accords-library.com`,
|
||||
password: process.env.RECORDER_DEFAULT_PASSWORD,
|
||||
const data: Omit<PayloadCreateData<Recorder>, "password" | "email"> = {
|
||||
username,
|
||||
anonymize,
|
||||
languages: languages.data?.map((language) => language.attributes.code),
|
||||
|
@ -49,7 +47,26 @@ export const importFromStrapi = createStrapiImportEndpoint<Recorder, StrapiRecor
|
|||
}),
|
||||
};
|
||||
|
||||
await payload.create({ collection: Collections.Recorders, data, user });
|
||||
const recorder = (
|
||||
await payload.find({
|
||||
collection: Collections.Recorders,
|
||||
where: { username: { equals: username } },
|
||||
})
|
||||
).docs[0] as Recorder | undefined;
|
||||
|
||||
if (isDefined(recorder)) {
|
||||
await payload.update({ collection: Collections.Recorders, id: recorder.id, data, user });
|
||||
} else {
|
||||
await payload.create({
|
||||
collection: Collections.Recorders,
|
||||
data: {
|
||||
...data,
|
||||
email: `${anonymous_code}@accords-library.com`,
|
||||
password: process.env.RECORDER_DEFAULT_PASSWORD,
|
||||
},
|
||||
user,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { CollectionGroups, Collections } from "../../constants";
|
||||
import { Collections } from "../../constants";
|
||||
import { backPropagationField } from "../../fields/backPropagationField/backPropagationField";
|
||||
import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig";
|
||||
|
||||
|
@ -7,6 +7,7 @@ const fields = {
|
|||
mimeType: "mimeType",
|
||||
filesize: "filesize",
|
||||
recorder: "recorder",
|
||||
updatedAt: "updatedAt",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
export const RecordersThumbnails = buildImageCollectionConfig({
|
||||
|
@ -15,27 +16,14 @@ export const RecordersThumbnails = buildImageCollectionConfig({
|
|||
singular: "Recorders Thumbnail",
|
||||
plural: "Recorders Thumbnails",
|
||||
},
|
||||
defaultSort: fields.filename,
|
||||
admin: {
|
||||
useAsTitle: fields.filename,
|
||||
disableDuplicate: true,
|
||||
group: CollectionGroups.Media,
|
||||
},
|
||||
admin: { defaultColumns: [fields.filename, fields.recorder, fields.updatedAt] },
|
||||
upload: {
|
||||
imageSizes: [
|
||||
{
|
||||
name: "og",
|
||||
height: 256,
|
||||
width: 256,
|
||||
formatOptions: {
|
||||
format: "jpg",
|
||||
options: { progressive: true, mozjpeg: true, compressionLevel: 9, quality: 80 },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "small",
|
||||
height: 128,
|
||||
width: 128,
|
||||
name: "square",
|
||||
height: 150,
|
||||
width: 150,
|
||||
fit: "cover",
|
||||
formatOptions: {
|
||||
format: "webp",
|
||||
options: { effort: 6, quality: 80, alphaQuality: 80 },
|
||||
|
|
|
@ -94,7 +94,6 @@ export const Videos: CollectionConfig = buildCollectionConfig({
|
|||
name: fields.channel,
|
||||
type: "relationship",
|
||||
relationTo: Collections.VideosChannels,
|
||||
required: true,
|
||||
admin: { position: "sidebar" },
|
||||
},
|
||||
],
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import payload from "payload";
|
||||
import { Collections } from "../../../constants";
|
||||
import { Collections, VideoSources } from "../../../constants";
|
||||
import { createStrapiImportEndpoint } from "../../../endpoints/createStrapiImportEndpoint";
|
||||
import { Video, VideosChannel } from "../../../types/collections";
|
||||
import { PayloadCreateData } from "../../../types/payload";
|
||||
import { isUndefined } from "../../../utils/asserts";
|
||||
import { isDefined, isUndefined } from "../../../utils/asserts";
|
||||
|
||||
type StapiVideo = {
|
||||
uid: string;
|
||||
|
@ -16,7 +16,7 @@ type StapiVideo = {
|
|||
};
|
||||
views: number;
|
||||
likes: number;
|
||||
source?: "YouTube" | "NicoNico" | "Tumblr";
|
||||
source?: VideoSources;
|
||||
gone: boolean;
|
||||
channel: { data?: { attributes: { uid: string; title: string; subscribers: number } } };
|
||||
};
|
||||
|
@ -43,31 +43,34 @@ export const importFromStrapi = createStrapiImportEndpoint<Video, StapiVideo>({
|
|||
user
|
||||
) => {
|
||||
if (isUndefined(source)) throw new Error("A source is required to create a Video");
|
||||
if (isUndefined(channel.data)) throw new Error("A channel is required to create a Video");
|
||||
if (source === VideoSources.YouTube && isUndefined(channel.data))
|
||||
throw new Error("A channel is required to create a YouTube Video");
|
||||
|
||||
try {
|
||||
const videoChannel: PayloadCreateData<VideosChannel> = {
|
||||
uid: channel.data.attributes.uid,
|
||||
title: channel.data.attributes.title,
|
||||
subscribers: channel.data.attributes.subscribers,
|
||||
};
|
||||
await payload.create({
|
||||
let videoChannelId;
|
||||
if (isDefined(channel.data)) {
|
||||
try {
|
||||
const videoChannel: PayloadCreateData<VideosChannel> = {
|
||||
uid: channel.data.attributes.uid,
|
||||
title: channel.data.attributes.title,
|
||||
subscribers: channel.data.attributes.subscribers,
|
||||
};
|
||||
await payload.create({
|
||||
collection: Collections.VideosChannels,
|
||||
data: videoChannel,
|
||||
user,
|
||||
});
|
||||
} catch (e) {}
|
||||
|
||||
const result = await payload.find({
|
||||
collection: Collections.VideosChannels,
|
||||
data: videoChannel,
|
||||
user,
|
||||
where: { uid: { equals: channel.data.attributes.uid } },
|
||||
});
|
||||
} catch (e) {}
|
||||
|
||||
const result = await payload.find({
|
||||
collection: Collections.VideosChannels,
|
||||
where: { uid: { equals: channel.data.attributes.uid } },
|
||||
});
|
||||
|
||||
if (result.docs.length === 0) {
|
||||
throw new Error("A video channel is required to create a video");
|
||||
if (result.docs.length > 0) {
|
||||
videoChannelId = result.docs[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
const videoChannel = result.docs[0] as VideosChannel;
|
||||
const video: PayloadCreateData<Video> = {
|
||||
uid,
|
||||
title,
|
||||
|
@ -77,7 +80,7 @@ export const importFromStrapi = createStrapiImportEndpoint<Video, StapiVideo>({
|
|||
gone,
|
||||
source,
|
||||
publishedDate: `${year}-${month}-${day}`,
|
||||
channel: videoChannel.id,
|
||||
channel: videoChannelId,
|
||||
};
|
||||
|
||||
await payload.create({
|
||||
|
|
|
@ -2,7 +2,6 @@ import { CollectionConfig } from "payload/types";
|
|||
import { mustBeAdmin } from "../../accesses/collections/mustBeAdmin";
|
||||
import { CollectionGroups, Collections } from "../../constants";
|
||||
import { buildCollectionConfig } from "../../utils/collectionConfig";
|
||||
import { importFromStrapi } from "./endpoints/importFromStrapi";
|
||||
|
||||
const fields = {
|
||||
uid: "uid",
|
||||
|
@ -28,7 +27,6 @@ export const VideosChannels: CollectionConfig = buildCollectionConfig({
|
|||
create: mustBeAdmin,
|
||||
delete: mustBeAdmin,
|
||||
},
|
||||
endpoints: [importFromStrapi],
|
||||
timestamps: false,
|
||||
fields: [
|
||||
{ name: fields.uid, type: "text", required: true, unique: true },
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import { Collections } from "../../../constants";
|
||||
import { createStrapiImportEndpoint } from "../../../endpoints/createStrapiImportEndpoint";
|
||||
import { VideosChannel } from "../../../types/collections";
|
||||
|
||||
type StrapiVideoChannel = {
|
||||
uid: string;
|
||||
title: string;
|
||||
subscribers: number;
|
||||
};
|
||||
|
||||
export const importFromStrapi = createStrapiImportEndpoint<VideosChannel, StrapiVideoChannel>({
|
||||
strapi: {
|
||||
collection: "video-channels",
|
||||
params: {},
|
||||
},
|
||||
payload: {
|
||||
collection: Collections.VideosChannels,
|
||||
convert: ({ uid, title, subscribers }) => ({
|
||||
uid,
|
||||
title,
|
||||
subscribers,
|
||||
}),
|
||||
},
|
||||
});
|
|
@ -1,5 +1,7 @@
|
|||
import { RowLabelArgs } from "payload/dist/admin/components/forms/RowLabel/types";
|
||||
import { CollectionGroups, Collections, KeysTypes } from "../../constants";
|
||||
import { createGetByEndpoint } from "../../endpoints/createGetByEndpoint";
|
||||
import { createGetSlugsEndpoint } from "../../endpoints/createGetSlugsEndpoint";
|
||||
import { imageField } from "../../fields/imageField/imageField";
|
||||
import { keysField } from "../../fields/keysField/keysField";
|
||||
import { slugField } from "../../fields/slugField/slugField";
|
||||
|
@ -32,8 +34,8 @@ export const Weapons = buildVersionedCollectionConfig({
|
|||
admin: {
|
||||
useAsTitle: fields.slug,
|
||||
defaultColumns: [
|
||||
fields.slug,
|
||||
fields.thumbnail,
|
||||
fields.slug,
|
||||
fields.group,
|
||||
fields.type,
|
||||
fields.appearances,
|
||||
|
@ -41,7 +43,11 @@ export const Weapons = buildVersionedCollectionConfig({
|
|||
],
|
||||
group: CollectionGroups.Collections,
|
||||
},
|
||||
endpoints: [importFromStrapi],
|
||||
endpoints: [
|
||||
importFromStrapi,
|
||||
createGetSlugsEndpoint(Collections.Weapons),
|
||||
createGetByEndpoint(Collections.Weapons, "slug"),
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
type: "row",
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { CollectionGroups, Collections } from "../../constants";
|
||||
import { Collections } from "../../constants";
|
||||
import { backPropagationField } from "../../fields/backPropagationField/backPropagationField";
|
||||
import { buildImageCollectionConfig } from "../../utils/imageCollectionConfig";
|
||||
|
||||
const fields = {
|
||||
filename: "filename",
|
||||
mimeType: "mimeType",
|
||||
filesize: "filesize",
|
||||
weapon: "weapon",
|
||||
updatedAt: "updatedAt",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
export const WeaponsThumbnails = buildImageCollectionConfig({
|
||||
|
@ -13,12 +16,7 @@ export const WeaponsThumbnails = buildImageCollectionConfig({
|
|||
singular: "Weapons Thumbnail",
|
||||
plural: "Weapons Thumbnails",
|
||||
},
|
||||
defaultSort: fields.filename,
|
||||
admin: {
|
||||
useAsTitle: fields.filename,
|
||||
disableDuplicate: true,
|
||||
group: CollectionGroups.Media,
|
||||
},
|
||||
admin: { defaultColumns: [fields.filename, fields.weapon, fields.updatedAt] },
|
||||
upload: {
|
||||
imageSizes: [
|
||||
{
|
||||
|
@ -62,5 +60,12 @@ export const WeaponsThumbnails = buildImageCollectionConfig({
|
|||
},
|
||||
],
|
||||
},
|
||||
fields: [],
|
||||
fields: [
|
||||
backPropagationField({
|
||||
name: fields.weapon,
|
||||
hasMany: false,
|
||||
relationTo: Collections.Weapons,
|
||||
where: ({ id }) => ({ thumbnail: { equals: id } }),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
.grid {
|
||||
margin-bottom: 25px;
|
||||
|
||||
> .grid__header {
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
> .grid__cells {
|
||||
display: grid;
|
||||
grid-gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
|
||||
> .grid__cells__cell {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
background-color: var(--theme-elevation-50);
|
||||
border: 1px solid var(--theme-elevation-100);
|
||||
position: relative;
|
||||
|
||||
> .grid__cells__cell__filename {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
|
||||
.thumbnail {
|
||||
> img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
object-fit: contain;
|
||||
background-size: 9% 9%;
|
||||
background-position: center;
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
var(--theme-elevation-100) 1px,
|
||||
var(--theme-elevation-0) 1px
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .grid__cells__cell__selector {
|
||||
position: absolute;
|
||||
left: 0.75rem;
|
||||
top: 0.75rem;
|
||||
}
|
||||
|
||||
> .grid__cells__cell__info {
|
||||
display: grid;
|
||||
line-height: 1.5;
|
||||
padding: 1rem;
|
||||
gap: 0.5rem;
|
||||
overflow-x: hidden;
|
||||
|
||||
> .grid__cells__cell__title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
> .grid__cells__cell__others {
|
||||
display: grid;
|
||||
line-height: 1.5;
|
||||
color: var(--theme-elevation-600);
|
||||
width: 100%;
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
import { useTableColumns } from "payload/dist/admin/components/elements/TableColumns";
|
||||
import React, { Fragment } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import "payload/dist/admin/components/elements/Table/index.scss";
|
||||
import { Column } from "payload/dist/admin/components/elements/Table/types";
|
||||
import Thumbnail from "payload/dist/admin/components/elements/Thumbnail";
|
||||
import { SanitizedCollectionConfig } from "payload/types";
|
||||
import "./index.scss";
|
||||
|
||||
const baseClass = "grid";
|
||||
|
||||
type Props = {
|
||||
data: any[];
|
||||
collection: SanitizedCollectionConfig;
|
||||
};
|
||||
|
||||
const fieldNames = {
|
||||
filename: "filename",
|
||||
select: "_select",
|
||||
};
|
||||
|
||||
export const Grid: React.FC<Props> = ({ data, collection }) => {
|
||||
const { columns: columnsFromContext } = useTableColumns();
|
||||
|
||||
const fields = columnsFromContext;
|
||||
const otherFields = fields?.filter(
|
||||
(col) => col.active && ![fieldNames.filename, fieldNames.select].includes(col.accessor)
|
||||
);
|
||||
|
||||
const filenameField = fields.find((col) => col.accessor === fieldNames.filename);
|
||||
const selectorField = fields.find((col) => col.accessor === fieldNames.select);
|
||||
|
||||
const headerColumns = fields
|
||||
.sort((a, b) => {
|
||||
const sortingValue = (value: Column) => {
|
||||
switch (value.accessor) {
|
||||
case fieldNames.select:
|
||||
return 2;
|
||||
case fieldNames.filename:
|
||||
return 1;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
return sortingValue(b) - sortingValue(a);
|
||||
})
|
||||
.filter(({ active, accessor }) => active || accessor === fieldNames.filename);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__header`}>
|
||||
{headerColumns.map((col, index) => (
|
||||
<div key={index} id={`heading-${col.accessor}`}>
|
||||
{col.components.Heading}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${baseClass}__cells`}>
|
||||
{data &&
|
||||
data.map((gridCell, cellIndex) => (
|
||||
<div key={cellIndex} className={`${baseClass}__cells__cell`}>
|
||||
{filenameField && (
|
||||
<Link
|
||||
className={`${baseClass}__cells__cell__filename`}
|
||||
to={`${collection.slug}/${gridCell.id}`}>
|
||||
<Thumbnail collection={collection} doc={gridCell} />
|
||||
</Link>
|
||||
)}
|
||||
{selectorField && (
|
||||
<div className={`${baseClass}__cells__cell__selector`}>
|
||||
{selectorField.components.renderCell(gridCell, gridCell[selectorField.accessor])}
|
||||
</div>
|
||||
)}
|
||||
<div className={`${baseClass}__cells__cell__info`}>
|
||||
{filenameField && (
|
||||
<Link
|
||||
className={`${baseClass}__cells__cell__title`}
|
||||
to={`${collection.slug}/${gridCell.id}`}>
|
||||
{String(gridCell[filenameField.accessor])}
|
||||
</Link>
|
||||
)}
|
||||
{otherFields.length > 0 && (
|
||||
<div className={`${baseClass}__cells__cell__others`}>
|
||||
{otherFields.map((col, colIndex) => (
|
||||
<Fragment key={colIndex}>
|
||||
{col.components.renderCell(gridCell, gridCell[col.accessor])}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Grid;
|
|
@ -0,0 +1,185 @@
|
|||
import { useWindowInfo } from "@faceless-ui/window-info";
|
||||
import Button from "payload/dist/admin/components/elements/Button";
|
||||
import DeleteMany from "payload/dist/admin/components/elements/DeleteMany";
|
||||
import EditMany from "payload/dist/admin/components/elements/EditMany";
|
||||
import Eyebrow from "payload/dist/admin/components/elements/Eyebrow";
|
||||
import { Gutter } from "payload/dist/admin/components/elements/Gutter";
|
||||
import ListControls from "payload/dist/admin/components/elements/ListControls";
|
||||
import ListSelection from "payload/dist/admin/components/elements/ListSelection";
|
||||
import Paginator from "payload/dist/admin/components/elements/Paginator";
|
||||
import PerPage from "payload/dist/admin/components/elements/PerPage";
|
||||
import Pill from "payload/dist/admin/components/elements/Pill";
|
||||
import PublishMany from "payload/dist/admin/components/elements/PublishMany";
|
||||
import { StaggeredShimmers } from "payload/dist/admin/components/elements/ShimmerEffect";
|
||||
import UnpublishMany from "payload/dist/admin/components/elements/UnpublishMany";
|
||||
import ViewDescription from "payload/dist/admin/components/elements/ViewDescription";
|
||||
import Meta from "payload/dist/admin/components/utilities/Meta";
|
||||
import { RelationshipProvider } from "payload/dist/admin/components/views/collections/List/RelationshipProvider";
|
||||
import { SelectionProvider } from "payload/dist/admin/components/views/collections/List/SelectionProvider";
|
||||
import { Props } from "payload/dist/admin/components/views/collections/List/types";
|
||||
import formatFilesize from "payload/dist/uploads/formatFilesize";
|
||||
import { getTranslation } from "payload/dist/utilities/getTranslation";
|
||||
import React, { Fragment } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Grid from "./Grid";
|
||||
|
||||
const baseClass = "collection-list";
|
||||
|
||||
export const UploadsGridView: React.ComponentType<Props> = (props) => {
|
||||
const {
|
||||
collection,
|
||||
collection: {
|
||||
labels: { singular: singularLabel, plural: pluralLabel },
|
||||
admin: {
|
||||
description,
|
||||
components: { BeforeList, BeforeListTable, AfterListTable, AfterList } = {},
|
||||
} = {},
|
||||
},
|
||||
data,
|
||||
newDocumentURL,
|
||||
limit,
|
||||
hasCreatePermission,
|
||||
disableEyebrow,
|
||||
modifySearchParams,
|
||||
handleSortChange,
|
||||
handleWhereChange,
|
||||
handlePageChange,
|
||||
handlePerPageChange,
|
||||
customHeader,
|
||||
resetParams,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
breakpoints: { s: smallBreak },
|
||||
} = useWindowInfo();
|
||||
const { t, i18n } = useTranslation("general");
|
||||
let formattedDocs = data.docs || [];
|
||||
|
||||
if (collection.upload) {
|
||||
formattedDocs = formattedDocs?.map((doc) => {
|
||||
return {
|
||||
...doc,
|
||||
filesize: formatFilesize(doc.filesize),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{Array.isArray(BeforeList) &&
|
||||
BeforeList.map((Component, i) => <Component key={i} {...props} />)}
|
||||
|
||||
<Meta title={getTranslation(collection.labels.plural, i18n)} />
|
||||
<SelectionProvider docs={data.docs} totalDocs={data.totalDocs}>
|
||||
{!disableEyebrow && <Eyebrow />}
|
||||
<Gutter className={`${baseClass}__wrap`}>
|
||||
<header className={`${baseClass}__header`}>
|
||||
{customHeader && customHeader}
|
||||
{!customHeader && (
|
||||
<Fragment>
|
||||
<h1>{getTranslation(pluralLabel, i18n)}</h1>
|
||||
{hasCreatePermission && (
|
||||
<Pill
|
||||
to={newDocumentURL}
|
||||
aria-label={t("createNewLabel", {
|
||||
label: getTranslation(singularLabel, i18n),
|
||||
})}>
|
||||
{t("createNew")}
|
||||
</Pill>
|
||||
)}
|
||||
{!smallBreak && (
|
||||
<ListSelection label={getTranslation(collection.labels.plural, i18n)} />
|
||||
)}
|
||||
{description && (
|
||||
<div className={`${baseClass}__sub-header`}>
|
||||
<ViewDescription description={description} />
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</header>
|
||||
<ListControls
|
||||
collection={collection}
|
||||
modifySearchQuery={modifySearchParams}
|
||||
handleSortChange={handleSortChange}
|
||||
handleWhereChange={handleWhereChange}
|
||||
resetParams={resetParams}
|
||||
/>
|
||||
{Array.isArray(BeforeListTable) &&
|
||||
BeforeListTable.map((Component, i) => <Component key={i} {...props} />)}
|
||||
{!data.docs && (
|
||||
<StaggeredShimmers
|
||||
className={[`${baseClass}__shimmer`, `${baseClass}__shimmer--rows`].join(" ")}
|
||||
count={6}
|
||||
/>
|
||||
)}
|
||||
{data.docs && data.docs.length > 0 && (
|
||||
<RelationshipProvider>
|
||||
<Grid data={formattedDocs} collection={collection} />
|
||||
</RelationshipProvider>
|
||||
)}
|
||||
{data.docs && data.docs.length === 0 && (
|
||||
<div className={`${baseClass}__no-results`}>
|
||||
<p>{t("noResults", { label: getTranslation(pluralLabel, i18n) })}</p>
|
||||
{hasCreatePermission && newDocumentURL && (
|
||||
<Button el="link" to={newDocumentURL}>
|
||||
{t("createNewLabel", { label: getTranslation(singularLabel, i18n) })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{Array.isArray(AfterListTable) &&
|
||||
AfterListTable.map((Component, i) => <Component key={i} {...props} />)}
|
||||
|
||||
<div className={`${baseClass}__page-controls`}>
|
||||
<Paginator
|
||||
limit={data.limit}
|
||||
totalPages={data.totalPages}
|
||||
page={data.page}
|
||||
hasPrevPage={data.hasPrevPage}
|
||||
hasNextPage={data.hasNextPage}
|
||||
prevPage={data.prevPage ?? undefined}
|
||||
nextPage={data.nextPage ?? undefined}
|
||||
numberOfNeighbors={1}
|
||||
disableHistoryChange={modifySearchParams === false}
|
||||
onChange={handlePageChange}
|
||||
/>
|
||||
{data?.totalDocs > 0 && (
|
||||
<Fragment>
|
||||
<div className={`${baseClass}__page-info`}>
|
||||
{data.page ?? 1 * data.limit - (data.limit - 1)}-
|
||||
{data.totalPages > 1 && data.totalPages !== data.page
|
||||
? data.limit * (data.page ?? 1)
|
||||
: data.totalDocs}{" "}
|
||||
{t("of")} {data.totalDocs}
|
||||
</div>
|
||||
<PerPage
|
||||
limits={collection?.admin?.pagination?.limits}
|
||||
limit={limit}
|
||||
modifySearchParams={modifySearchParams}
|
||||
handleChange={handlePerPageChange}
|
||||
resetPage={data.totalDocs <= data.pagingCounter}
|
||||
/>
|
||||
<div className={`${baseClass}__list-selection`}>
|
||||
{smallBreak && (
|
||||
<Fragment>
|
||||
<ListSelection label={getTranslation(collection.labels.plural, i18n)} />
|
||||
<div className={`${baseClass}__list-selection-actions`}>
|
||||
<EditMany collection={collection} resetParams={resetParams} />
|
||||
<PublishMany collection={collection} resetParams={resetParams} />
|
||||
<UnpublishMany collection={collection} resetParams={resetParams} />
|
||||
<DeleteMany collection={collection} resetParams={resetParams} />
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</Gutter>
|
||||
</SelectionProvider>
|
||||
{Array.isArray(AfterList) &&
|
||||
AfterList.map((Component, i) => <Component key={i} {...props} />)}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -79,6 +79,7 @@ export enum LibraryItemsTextualPageOrders {
|
|||
export enum RecordersRoles {
|
||||
Admin = "Admin",
|
||||
Recorder = "Recorder",
|
||||
Api = "Api",
|
||||
}
|
||||
|
||||
export enum CollectionStatus {
|
||||
|
|
|
@ -4,7 +4,7 @@ import { CollectionEndpoint } from "../types/payload";
|
|||
export const createGetByEndpoint = <T, R>(
|
||||
collection: string,
|
||||
attribute: string,
|
||||
handler: (doc: T) => Promise<R>
|
||||
handler: (doc: T) => Promise<R> | R = (doc) => doc as unknown as R
|
||||
): CollectionEndpoint => ({
|
||||
path: `/${attribute}/:${attribute}`,
|
||||
method: "get",
|
|
@ -0,0 +1,39 @@
|
|||
import payload from "payload";
|
||||
import { mustBeApi } from "../accesses/endpoints/mustBeApi";
|
||||
import { Collections } from "../constants";
|
||||
import { CollectionEndpoint } from "../types/payload";
|
||||
|
||||
export const createGetSlugsEndpoint = (collection: Collections): CollectionEndpoint => ({
|
||||
path: "/slugs",
|
||||
method: "get",
|
||||
handler: async (req, res) => {
|
||||
if (!mustBeApi(req)) {
|
||||
return res.status(403).send({
|
||||
errors: [
|
||||
{
|
||||
message: "You are not allowed to perform this action.",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
let page = 1;
|
||||
let totalPage = 1;
|
||||
const slugs: string[] = [];
|
||||
|
||||
while (page <= totalPage) {
|
||||
const entries = await payload.find({
|
||||
collection,
|
||||
page,
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
entries.docs.forEach(({ slug }: { slug: string }) => slugs.push(slug));
|
||||
|
||||
totalPage = entries.totalPages;
|
||||
page++;
|
||||
}
|
||||
|
||||
res.status(200).json(slugs);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
import payload from "payload";
|
||||
import { mustBeAdmin } from "../accesses/endpoints/mustBeAdmin";
|
||||
import { Collections } from "../constants";
|
||||
import { CollectionEndpoint } from "../types/payload";
|
||||
import { isDefined } from "../utils/asserts";
|
||||
|
||||
type Image = {
|
||||
filename: string;
|
||||
id: string | number;
|
||||
};
|
||||
|
||||
export const createImageRegenerationEndpoint = (collection: Collections): CollectionEndpoint => ({
|
||||
method: "get",
|
||||
path: "/regenerate",
|
||||
handler: async (req, res) => {
|
||||
if (!mustBeAdmin(req)) {
|
||||
return res.status(403).send({
|
||||
errors: [
|
||||
{
|
||||
message: "You are not allowed to perform this action.",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
let page = 1;
|
||||
let totalPage = 1;
|
||||
let count = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
while (page <= totalPage) {
|
||||
const images = await payload.find({
|
||||
collection,
|
||||
page,
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
images.docs.map(async (image: Image) => {
|
||||
try {
|
||||
await payload.update({
|
||||
collection,
|
||||
id: image.id,
|
||||
data: {},
|
||||
filePath: `uploads/${collection}/${image.filename}`,
|
||||
overwriteExistingFiles: true,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
if (typeof e === "object" && isDefined(e) && "name" in e) {
|
||||
errors.push(`${e.name} with ${image.id}`);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
totalPage = images.totalPages;
|
||||
count += images.docs.length;
|
||||
page++;
|
||||
}
|
||||
|
||||
res
|
||||
.status(200)
|
||||
.json({ message: `${count} entries have been regenerated successfully.`, errors });
|
||||
},
|
||||
});
|
|
@ -40,7 +40,7 @@ export const backPropagationField = ({
|
|||
if (hasMany) {
|
||||
return result.docs.map((doc) => doc.id);
|
||||
} else {
|
||||
return result.docs[0].id;
|
||||
return result.docs[0]?.id;
|
||||
}
|
||||
}
|
||||
return hasMany ? [] : undefined;
|
||||
|
|
|
@ -1,10 +1,23 @@
|
|||
import { RelationshipField, UploadField } from "payload/types";
|
||||
import { Collections } from "../../constants";
|
||||
import { FieldBase, RelationshipField } from "payload/dist/fields/config/types";
|
||||
import { Collections, FileTypes } from "../../constants";
|
||||
|
||||
type Props = Omit<UploadField, "type" | "relationTo">;
|
||||
type FileField = FieldBase & {
|
||||
relationTo: FileTypes;
|
||||
hasMany?: boolean;
|
||||
admin?: RelationshipField["admin"];
|
||||
};
|
||||
|
||||
export const fileField = (props: Props): RelationshipField => ({
|
||||
export const fileField = ({
|
||||
relationTo,
|
||||
hasMany = false,
|
||||
...props
|
||||
}: FileField): RelationshipField => ({
|
||||
...props,
|
||||
type: "relationship",
|
||||
hasMany: hasMany,
|
||||
relationTo: Collections.Files,
|
||||
filterOptions: { type: { equals: getFileTypesKey(relationTo) } },
|
||||
});
|
||||
|
||||
const getFileTypesKey = (fileType: FileTypes): string | undefined =>
|
||||
Object.entries(FileTypes).find(([, value]) => value === fileType)?.[0];
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Collections, KeysTypes } from "../../constants";
|
|||
type KeysField = FieldBase & {
|
||||
relationTo: KeysTypes;
|
||||
hasMany?: boolean;
|
||||
admin: RelationshipField["admin"];
|
||||
admin?: RelationshipField["admin"];
|
||||
};
|
||||
|
||||
export const keysField = ({
|
||||
|
|
|
@ -120,6 +120,11 @@ export interface LibraryItem {
|
|||
};
|
||||
audio?: {
|
||||
audioSubtype?: string[] | Key[];
|
||||
tracks?: {
|
||||
title: string;
|
||||
file: string | File;
|
||||
id?: string;
|
||||
}[];
|
||||
};
|
||||
releaseDate?: string;
|
||||
categories?: string[] | Key[];
|
||||
|
@ -159,6 +164,7 @@ export interface LibraryItem {
|
|||
}
|
||||
export interface LibraryItemThumbnail {
|
||||
id: string;
|
||||
libraryItem?: string[] | LibraryItem[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string;
|
||||
|
@ -304,6 +310,13 @@ export interface Language {
|
|||
id: string;
|
||||
name: string;
|
||||
}
|
||||
export interface File {
|
||||
id: string;
|
||||
filename: string;
|
||||
type: "LibraryScans" | "LibrarySoundtracks" | "ContentVideo" | "ContentAudio";
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface Currency {
|
||||
id: string;
|
||||
}
|
||||
|
@ -337,6 +350,7 @@ export interface Content {
|
|||
}
|
||||
export interface ContentsThumbnail {
|
||||
id: string;
|
||||
contents?: string[] | Content[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string;
|
||||
|
@ -370,6 +384,14 @@ export interface ContentsThumbnail {
|
|||
filesize?: number;
|
||||
filename?: string;
|
||||
};
|
||||
max?: {
|
||||
url?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
mimeType?: string;
|
||||
filesize?: number;
|
||||
filename?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
export interface Recorder {
|
||||
|
@ -378,7 +400,7 @@ export interface Recorder {
|
|||
avatar?: string | RecordersThumbnail;
|
||||
languages?: string[] | Language[];
|
||||
biographies?: RecorderBiographies;
|
||||
role?: ("Admin" | "Recorder")[];
|
||||
role?: ("Admin" | "Recorder" | "Api")[];
|
||||
anonymize: boolean;
|
||||
email: string;
|
||||
resetPasswordToken?: string;
|
||||
|
@ -409,15 +431,7 @@ export interface RecordersThumbnail {
|
|||
filesize?: number;
|
||||
filename?: string;
|
||||
};
|
||||
og?: {
|
||||
url?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
mimeType?: string;
|
||||
filesize?: number;
|
||||
filename?: string;
|
||||
};
|
||||
small?: {
|
||||
square?: {
|
||||
url?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
|
@ -614,13 +628,6 @@ export interface Tabs_Tab_Section_Section_Section_Section {
|
|||
blockName?: string;
|
||||
blockType: "section";
|
||||
}
|
||||
export interface File {
|
||||
id: string;
|
||||
filename: string;
|
||||
type: "LibraryScans" | "LibrarySoundtracks" | "ContentVideo" | "ContentAudio";
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface ContentsFolder {
|
||||
id: string;
|
||||
slug: string;
|
||||
|
@ -671,6 +678,7 @@ export interface Post {
|
|||
}
|
||||
export interface PostThumbnail {
|
||||
id: string;
|
||||
posts?: string[] | Post[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string;
|
||||
|
@ -788,6 +796,7 @@ export interface Weapon {
|
|||
}
|
||||
export interface WeaponsThumbnail {
|
||||
id: string;
|
||||
weapon?: string | Weapon;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string;
|
||||
|
@ -859,7 +868,7 @@ export interface Video {
|
|||
likes?: number;
|
||||
views?: number;
|
||||
publishedDate: string;
|
||||
channel: string | VideosChannel;
|
||||
channel?: string | VideosChannel;
|
||||
}
|
||||
export interface VideosChannel {
|
||||
id: string;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { CollectionConfig } from "payload/types";
|
||||
import { CollectionConfig, PayloadRequest } from "payload/types";
|
||||
|
||||
export type PayloadCreateData<T> = Omit<
|
||||
T,
|
||||
|
@ -6,3 +6,5 @@ export type PayloadCreateData<T> = Omit<
|
|||
>;
|
||||
|
||||
export type CollectionEndpoint = NonNullable<CollectionConfig["endpoints"]>[number];
|
||||
|
||||
export type EndpointAccess<U> = (req: PayloadRequest<U>) => boolean;
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import { ImageSize } from "payload/dist/uploads/types";
|
||||
import { CollectionConfig } from "payload/types";
|
||||
import { UploadsGridView } from "../components/UploadsGridView/UploadsGridView";
|
||||
import { CollectionGroups } from "../constants";
|
||||
import { createImageRegenerationEndpoint } from "../endpoints/createImageRegenerationEndpoint";
|
||||
import { BuildCollectionConfig, buildCollectionConfig } from "./collectionConfig";
|
||||
|
||||
type BuildImageCollectionConfig = Omit<BuildCollectionConfig, "upload"> & {
|
||||
|
@ -7,11 +10,21 @@ type BuildImageCollectionConfig = Omit<BuildCollectionConfig, "upload"> & {
|
|||
};
|
||||
|
||||
export const buildImageCollectionConfig = ({
|
||||
admin,
|
||||
upload: { imageSizes },
|
||||
...otherConfig
|
||||
}: BuildImageCollectionConfig): CollectionConfig =>
|
||||
buildCollectionConfig({
|
||||
...otherConfig,
|
||||
defaultSort: "-updatedAt",
|
||||
admin: {
|
||||
disableDuplicate: true,
|
||||
useAsTitle: "filename",
|
||||
group: CollectionGroups.Media,
|
||||
components: { views: { List: UploadsGridView } },
|
||||
...admin,
|
||||
},
|
||||
endpoints: [createImageRegenerationEndpoint(otherConfig.slug)],
|
||||
upload: {
|
||||
staticDir: `../uploads/${otherConfig.slug}`,
|
||||
mimeTypes: ["image/*"],
|
||||
|
|
|
@ -97,8 +97,8 @@
|
|||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
"alwaysStrict": true /* Ensure 'use strict' is always emitted. */,
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
"noUnusedLocals": true /* Enable error reporting when local variables aren't read. */,
|
||||
"noUnusedParameters": true /* Raise an error when a function parameter isn't read. */,
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
"noFallthroughCasesInSwitch": true /* Enable error reporting for fallthrough cases in switch statements. */,
|
||||
|
|
Loading…
Reference in New Issue