Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
+648
@@ -0,0 +1,648 @@
|
||||
import type { KbArticleDto } from "../dtos/KbArticleDto";
|
||||
import type { KbCategoryDto } from "../dtos/KbCategoryDto";
|
||||
import type { KbSearchResultDto } from "../dtos/KbSearchResultDto";
|
||||
import type { KnowledgeBaseDto } from "../dtos/KnowledgeBaseDto";
|
||||
import { KnowledgeBaseWithDetails, getAllKnowledgeBases, getKnowledgeBaseById, getKnowledgeBaseBySlug } from "../db/knowledgeBase.db.server";
|
||||
import { db } from "~/utils/db.server";
|
||||
import { KbNavLinkDto } from "../dtos/KbNavLinkDto";
|
||||
import {
|
||||
KnowledgeBaseArticleWithDetails,
|
||||
createKnowledgeBaseArticle,
|
||||
getAllKnowledgeBaseArticles,
|
||||
getFeaturedKnowledgeBaseArticles,
|
||||
getKbArticleByIdInKnowledgeBase,
|
||||
getKbArticleBySlug,
|
||||
} from "../db/kbArticles.db.server";
|
||||
import { searchKnowledgeBaseArticles } from "../services/kbArticleSearch.server";
|
||||
import { createKnowledgeBaseCategory, getAllKnowledgeBaseCategories, getKbCategoryById, getKbCategoryBySlug } from "../db/kbCategories.db.server";
|
||||
import KnowledgeBaseUtils from "../utils/KnowledgeBaseUtils";
|
||||
import { KnowledgeBaseCategoryWithDetails } from "../helpers/KbCategoryModelHelper";
|
||||
import { KnowledgeBaseRelatedArticle } from "@toolctrl/db/prisma-browser";
|
||||
import { defaultLocale, isSupportedLocale } from "@toolctrl/i18n";
|
||||
import { marked } from "marked";
|
||||
import { htmlToText } from "html-to-text";
|
||||
import { remixI18Next } from "~/locale/i18next.server";
|
||||
import { cachified } from "~/utils/cache.server";
|
||||
import { getBaseURL } from "~/utils/url.server";
|
||||
|
||||
export type KnowledgeBaseServiceErrorCode =
|
||||
| "cannot-delete-with-articles"
|
||||
| "cannot-delete-with-categories"
|
||||
| "disabled"
|
||||
| "invalid-category"
|
||||
| "invalid-article"
|
||||
| "invalid-section"
|
||||
| "not-found";
|
||||
|
||||
const knowledgeBaseServiceErrorKeys: Record<KnowledgeBaseServiceErrorCode, string> = {
|
||||
"cannot-delete-with-articles": "knowledgeBase.admin.errors.cannotDeleteWithArticles",
|
||||
"cannot-delete-with-categories": "knowledgeBase.admin.errors.cannotDeleteWithCategories",
|
||||
disabled: "knowledgeBase.public.errors.disabled",
|
||||
"invalid-category": "knowledgeBase.adminCategories.errors.categoryNotFound",
|
||||
"invalid-article": "knowledgeBase.adminCategories.errors.articleNotFound",
|
||||
"invalid-section": "knowledgeBase.adminCategories.errors.sectionNotFound",
|
||||
"not-found": "knowledgeBase.public.errors.notFound",
|
||||
};
|
||||
|
||||
export class KnowledgeBaseServiceError extends Error {
|
||||
constructor(readonly code: KnowledgeBaseServiceErrorCode) {
|
||||
super(code);
|
||||
this.name = "KnowledgeBaseServiceError";
|
||||
}
|
||||
}
|
||||
|
||||
export function getKnowledgeBaseActionErrorKey(error: unknown): string {
|
||||
if (error instanceof KnowledgeBaseServiceError) {
|
||||
return knowledgeBaseServiceErrorKeys[error.code];
|
||||
}
|
||||
return "knowledgeBase.admin.errors.actionFailed";
|
||||
}
|
||||
|
||||
export function getKnowledgeBasePublicErrorKey(error: unknown): string {
|
||||
if (error instanceof KnowledgeBaseServiceError && (error.code === "not-found" || error.code === "disabled")) {
|
||||
return knowledgeBaseServiceErrorKeys[error.code];
|
||||
}
|
||||
return "knowledgeBase.public.errors.unavailable";
|
||||
}
|
||||
|
||||
async function getAll({ enabled, request }: { enabled?: boolean; request: Request }): Promise<KnowledgeBaseDto[]> {
|
||||
const items = await getAllKnowledgeBases({ enabled });
|
||||
return items.map((item) => kbToDto(item, request));
|
||||
}
|
||||
|
||||
async function get({ slug, enabled, request }: { slug: string; enabled?: boolean; request: Request }): Promise<KnowledgeBaseDto> {
|
||||
const item = await cachified({
|
||||
key: `kb:${slug}`,
|
||||
ttl: 60 * 60 * 24,
|
||||
getFreshValue: () => getKnowledgeBaseBySlug(slug),
|
||||
});
|
||||
if (!item) {
|
||||
throw new KnowledgeBaseServiceError("not-found");
|
||||
}
|
||||
|
||||
if (enabled !== undefined && item.enabled !== enabled) {
|
||||
throw new KnowledgeBaseServiceError("disabled");
|
||||
}
|
||||
return kbToDto(item, request);
|
||||
}
|
||||
|
||||
async function getById({ id, request }: { id: string; request: Request }): Promise<KnowledgeBaseDto | null> {
|
||||
const item = await getKnowledgeBaseById(id);
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return kbToDto(item, request);
|
||||
}
|
||||
|
||||
function kbToDto(kb: KnowledgeBaseWithDetails, request: Request) {
|
||||
const baseUrl = getBaseURL(request, { https: true });
|
||||
const item: KnowledgeBaseDto = {
|
||||
id: kb.id,
|
||||
createdAt: kb.createdAt,
|
||||
updatedAt: kb.updatedAt,
|
||||
basePath: kb.basePath,
|
||||
slug: kb.slug,
|
||||
title: kb.title,
|
||||
description: kb.description,
|
||||
defaultLanguage: kb.defaultLanguage,
|
||||
layout: kb.layout as "list" | "articles" | "grid",
|
||||
languages: JSON.parse(kb.languages) as string[],
|
||||
links: JSON.parse(kb.links) as KbNavLinkDto[],
|
||||
color: kb.color,
|
||||
enabled: kb.enabled,
|
||||
count: {
|
||||
articles: kb._count.articles,
|
||||
categories: kb._count.categories,
|
||||
views: kb._count.views,
|
||||
},
|
||||
logo: kb.logo,
|
||||
seoImage: kb.seoImage,
|
||||
metatags: [
|
||||
{ title: `${kb.title}` },
|
||||
{ name: "description", content: kb.description },
|
||||
// { name: "keywords", content: kb.keywords },
|
||||
{ property: "og:title", content: `${kb.title}` },
|
||||
{ property: "og:description", content: kb.description },
|
||||
{ property: "og:image", content: kb.seoImage },
|
||||
{ property: "og:type", content: "website" },
|
||||
{ property: "og:url", content: `${baseUrl}${KnowledgeBaseUtils.getKbUrl({ kb, params: {} })}` },
|
||||
{ property: "twitter:title", content: `${kb.title}` },
|
||||
{ property: "twitter:description", content: kb.description },
|
||||
{ property: "twitter:image", content: kb.seoImage },
|
||||
{ property: "twitter:card", content: "summary_large_image" },
|
||||
{
|
||||
tagName: "link",
|
||||
rel: "canonical",
|
||||
href: `${baseUrl}${KnowledgeBaseUtils.getKbUrl({ kb, params: {} })}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
return item;
|
||||
}
|
||||
|
||||
async function getCategories({
|
||||
kb,
|
||||
params,
|
||||
request,
|
||||
}: {
|
||||
kb: KnowledgeBaseDto;
|
||||
params: {
|
||||
lang?: string;
|
||||
};
|
||||
request: Request;
|
||||
}): Promise<KbCategoryDto[]> {
|
||||
const items = await getAllKnowledgeBaseCategories({
|
||||
knowledgeBaseSlug: kb.slug,
|
||||
language: params.lang || kb.defaultLanguage,
|
||||
});
|
||||
items.forEach((category) => {
|
||||
category.articles = category.articles.filter((f) => f.publishedAt);
|
||||
});
|
||||
|
||||
return items.map((f) => categoryToDto({ kb, category: f, params, request }));
|
||||
// let allItems = await generateFakeData(kb);
|
||||
// allItems.categories = allItems.categories.filter((f) => f.language === language);
|
||||
// const result = await fakeSearch({ original: allItems, query });
|
||||
// allItems.categories.forEach((category) => {
|
||||
// category.articles = allItems.articles;
|
||||
// });
|
||||
// return result.categories;
|
||||
}
|
||||
|
||||
function categoryToDto({
|
||||
kb,
|
||||
category,
|
||||
params,
|
||||
request,
|
||||
}: {
|
||||
kb: KnowledgeBaseDto;
|
||||
category: KnowledgeBaseCategoryWithDetails;
|
||||
params: { lang?: string };
|
||||
request: Request;
|
||||
}) {
|
||||
const baseUrl = getBaseURL(request, { https: true });
|
||||
const item: KbCategoryDto = {
|
||||
id: category.id,
|
||||
slug: category.slug,
|
||||
order: category.order,
|
||||
title: category.title,
|
||||
description: category.description,
|
||||
icon: category.icon,
|
||||
language: category.language,
|
||||
seoImage: category.seoImage,
|
||||
href: KnowledgeBaseUtils.getCategoryUrl({ kb, category, params }),
|
||||
sections: category.sections.map((f) => ({
|
||||
id: f.id,
|
||||
order: f.order,
|
||||
title: f.title,
|
||||
description: f.description,
|
||||
})),
|
||||
articles: category.articles.map((f) => ({
|
||||
id: f.id,
|
||||
order: f.order,
|
||||
title: f.title,
|
||||
description: f.description,
|
||||
language: f.language,
|
||||
slug: f.slug,
|
||||
href: KnowledgeBaseUtils.getArticleUrl({ kb, article: f, params }),
|
||||
sectionId: f.sectionId,
|
||||
publishedAt: f.publishedAt,
|
||||
categoryId: f.id,
|
||||
})),
|
||||
metatags: [
|
||||
{ title: `${category.title} | ${kb.title}` },
|
||||
{ name: "description", content: category.description },
|
||||
// { name: "keywords", content: category.keywords },
|
||||
{ property: "og:title", content: `${category.title} | ${kb.title}` },
|
||||
{ property: "og:description", content: category.description },
|
||||
{ property: "og:image", content: category.seoImage },
|
||||
{ property: "og:type", content: "website" },
|
||||
{ property: "og:url", content: `${baseUrl}${KnowledgeBaseUtils.getCategoryUrl({ kb, category, params })}` },
|
||||
{ property: "twitter:title", content: `${category.title} | ${kb.title}` },
|
||||
{ property: "twitter:description", content: category.description },
|
||||
{ property: "twitter:image", content: category.seoImage },
|
||||
{ property: "twitter:card", content: "summary_large_image" },
|
||||
{
|
||||
tagName: "link",
|
||||
rel: "canonical",
|
||||
href: `${baseUrl}${KnowledgeBaseUtils.getCategoryUrl({ kb, category, params })}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
return item;
|
||||
}
|
||||
|
||||
async function getArticles({
|
||||
kb,
|
||||
categoryId,
|
||||
language,
|
||||
params,
|
||||
request,
|
||||
}: {
|
||||
kb: KnowledgeBaseDto;
|
||||
categoryId: string;
|
||||
language: string;
|
||||
query: string | undefined;
|
||||
params: { lang?: string };
|
||||
request: Request;
|
||||
}): Promise<KbArticleDto[]> {
|
||||
const items = await getAllKnowledgeBaseArticles({
|
||||
knowledgeBaseSlug: kb.slug,
|
||||
categoryId,
|
||||
language,
|
||||
});
|
||||
return items.map((f) => articleToDto({ kb, article: f, relatedArticles: f.relatedArticles, params, request }));
|
||||
// let allItems = await generateFakeData(kb);
|
||||
// allItems.articles = allItems.articles.filter((f) => f.language === language);
|
||||
// const result = await fakeSearch({ original: allItems, query });
|
||||
// return result.articles;
|
||||
}
|
||||
|
||||
async function getArticle({ kb, slug, params, request }: { kb: KnowledgeBaseDto; slug: string; params: { lang?: string }; request: Request }): Promise<{
|
||||
article: KbArticleDto;
|
||||
category: KbCategoryDto;
|
||||
} | null> {
|
||||
let language = params.lang || kb.defaultLanguage;
|
||||
const item = await getKbArticleBySlug({
|
||||
knowledgeBaseId: kb.id,
|
||||
slug,
|
||||
language,
|
||||
});
|
||||
if (!item || !item.publishedAt) {
|
||||
return null;
|
||||
}
|
||||
if (!item.categoryId) {
|
||||
return null;
|
||||
}
|
||||
const category = await getKbCategoryById(item.categoryId);
|
||||
if (!category) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
article: articleToDto({ kb, article: item, relatedArticles: item.relatedArticles, params, request }),
|
||||
category: categoryToDto({ kb, category, params, request }),
|
||||
};
|
||||
// const getIdFromSlug = (slug: string) => {
|
||||
// return slug.split("-").pop();
|
||||
// };
|
||||
// const id = getIdFromSlug(slug);
|
||||
// const allItems = await generateFakeData(kb);
|
||||
// const article = allItems.articles.find((f) => f.id === id && f.language === language);
|
||||
|
||||
// if (!article) {
|
||||
// return null;
|
||||
// }
|
||||
// const category = allItems.categories[0];
|
||||
// return { article, category };
|
||||
}
|
||||
|
||||
async function getArticleById({
|
||||
kb,
|
||||
id,
|
||||
params,
|
||||
request,
|
||||
}: {
|
||||
kb: KnowledgeBaseDto;
|
||||
id: string;
|
||||
params?: { lang?: string };
|
||||
request: Request;
|
||||
}): Promise<KbArticleDto | null> {
|
||||
const item = await getKbArticleByIdInKnowledgeBase({ id, knowledgeBaseId: kb.id, language: params?.lang || kb.defaultLanguage });
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return articleToDto({ kb, article: item, relatedArticles: item.relatedArticles, params: {}, request });
|
||||
}
|
||||
|
||||
async function getFeaturedArticles({ kb, params, request }: { kb: KnowledgeBaseDto; params: { lang?: string }; request: Request }) {
|
||||
const items = await getFeaturedKnowledgeBaseArticles({
|
||||
knowledgeBaseId: kb.id,
|
||||
language: params.lang || kb.defaultLanguage,
|
||||
});
|
||||
return items.map((f) => articleToDto({ kb, article: f, relatedArticles: f.relatedArticles, params, request }));
|
||||
// let allItems = await generateFakeData(kb);
|
||||
// allItems.articles = allItems.articles.filter((f) => f.language === language && f.featuredOrder);
|
||||
// // return first 6
|
||||
// if (allItems.articles.length <= 4) {
|
||||
// return allItems.articles;
|
||||
// }
|
||||
// return allItems.articles.slice(0, 4);
|
||||
}
|
||||
|
||||
function articleToDto({
|
||||
kb,
|
||||
article,
|
||||
relatedArticles,
|
||||
params,
|
||||
request,
|
||||
}: {
|
||||
kb: KnowledgeBaseDto;
|
||||
article: KnowledgeBaseArticleWithDetails;
|
||||
relatedArticles: (KnowledgeBaseRelatedArticle & {
|
||||
relatedArticle: { id: string; order: number; title: string; slug: string };
|
||||
})[];
|
||||
params: { lang?: string };
|
||||
request: Request;
|
||||
}) {
|
||||
const baseUrl = getBaseURL(request, { https: true });
|
||||
const item: KbArticleDto = {
|
||||
id: article.id,
|
||||
createdAt: article.createdAt,
|
||||
updatedAt: article.updatedAt,
|
||||
slug: article.slug,
|
||||
title: article.title,
|
||||
description: article.description,
|
||||
order: article.order,
|
||||
contentDraft: article.contentDraft,
|
||||
contentPublished: article.contentPublished,
|
||||
contentPublishedAsText: article.contentPublishedAsText,
|
||||
contentType: article.contentType as "wysiwyg" | "markdown",
|
||||
language: article.language,
|
||||
featuredOrder: article.featuredOrder,
|
||||
createdByUser: article.createdByUser,
|
||||
publishedAt: article.publishedAt,
|
||||
href: KnowledgeBaseUtils.getArticleUrl({ kb, article, params }),
|
||||
sectionId: article.sectionId,
|
||||
relatedArticles: relatedArticles.map((f) => ({
|
||||
order: f.relatedArticle.order,
|
||||
title: f.relatedArticle.title,
|
||||
slug: f.relatedArticle.slug,
|
||||
href: KnowledgeBaseUtils.getArticleUrl({ kb, article: f.relatedArticle, params }),
|
||||
})),
|
||||
seoImage: article.seoImage,
|
||||
metatags: [
|
||||
{ title: `${article.title} | ${kb.title}` },
|
||||
{ name: "description", content: article.description },
|
||||
// { name: "keywords", content: article.keywords },
|
||||
{ property: "og:title", content: `${article.title}` },
|
||||
{ property: "og:description", content: article.description },
|
||||
{ property: "og:image", content: article.seoImage },
|
||||
{ property: "og:type", content: "article" },
|
||||
{ property: "og:url", content: `${baseUrl}${KnowledgeBaseUtils.getArticleUrl({ kb, article, params })}` },
|
||||
{ property: "twitter:title", content: `${article.title}` },
|
||||
{ property: "twitter:description", content: article.description },
|
||||
{ property: "twitter:image", content: article.seoImage },
|
||||
{ property: "twitter:card", content: "summary_large_image" },
|
||||
{
|
||||
tagName: "link",
|
||||
rel: "canonical",
|
||||
href: `${baseUrl}${KnowledgeBaseUtils.getArticleUrl({ kb, article, params })}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
return item;
|
||||
}
|
||||
|
||||
async function getCategory({
|
||||
kb,
|
||||
category,
|
||||
language,
|
||||
params,
|
||||
request,
|
||||
}: {
|
||||
kb: KnowledgeBaseDto;
|
||||
category: string;
|
||||
language: string;
|
||||
params: { lang?: string };
|
||||
request: Request;
|
||||
}): Promise<KbCategoryDto | null> {
|
||||
const item = await getKbCategoryBySlug({
|
||||
knowledgeBaseId: kb.id,
|
||||
slug: category,
|
||||
language,
|
||||
});
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
item.articles = item.articles.filter((f) => f.publishedAt);
|
||||
return categoryToDto({ kb, category: item, params, request });
|
||||
}
|
||||
|
||||
async function del(item: KnowledgeBaseDto) {
|
||||
const articlesCount = await db.knowledgeBaseArticle.count({
|
||||
where: { knowledgeBaseId: item.id },
|
||||
});
|
||||
const categoriesCount = await db.knowledgeBaseCategory.count({
|
||||
where: { knowledgeBaseId: item.id },
|
||||
});
|
||||
if (articlesCount > 0) {
|
||||
throw new KnowledgeBaseServiceError("cannot-delete-with-articles");
|
||||
}
|
||||
if (categoriesCount > 0) {
|
||||
throw new KnowledgeBaseServiceError("cannot-delete-with-categories");
|
||||
}
|
||||
return await db.knowledgeBase.delete({
|
||||
where: { id: item.id },
|
||||
});
|
||||
}
|
||||
|
||||
async function duplicateCategory({ kb, language, categoryId }: { kb: KnowledgeBaseDto; language: string; categoryId: string }) {
|
||||
const allCategories = await getAllKnowledgeBaseCategories({
|
||||
knowledgeBaseSlug: kb.slug,
|
||||
language,
|
||||
});
|
||||
const existing = allCategories.find((p) => p.id === categoryId);
|
||||
if (!existing) {
|
||||
throw new KnowledgeBaseServiceError("invalid-category");
|
||||
}
|
||||
let number = 2;
|
||||
let slug = "";
|
||||
|
||||
const findExistingSlug = (slug: string) => {
|
||||
return allCategories.find((p) => p.slug === slug);
|
||||
};
|
||||
|
||||
do {
|
||||
slug = `${existing.slug}${number}`;
|
||||
const existingWithSlug = findExistingSlug(slug);
|
||||
if (!existingWithSlug) {
|
||||
break;
|
||||
}
|
||||
// if (number > 10) {
|
||||
// throw Error("Too many duplicates");
|
||||
// }
|
||||
number++;
|
||||
} while (true);
|
||||
|
||||
let maxOrder = 0;
|
||||
if (allCategories.length > 0) {
|
||||
maxOrder = Math.max(...allCategories.map((p) => p.order));
|
||||
}
|
||||
|
||||
const item = await createKnowledgeBaseCategory({
|
||||
knowledgeBaseId: kb.id,
|
||||
slug,
|
||||
order: maxOrder + 1,
|
||||
title: existing.title + " " + number,
|
||||
description: existing.description,
|
||||
icon: existing.icon,
|
||||
language: existing.language,
|
||||
seoImage: existing.seoImage,
|
||||
});
|
||||
return item;
|
||||
}
|
||||
|
||||
async function duplicateArticle({ kb, language, articleId }: { kb: KnowledgeBaseDto; language: string; articleId: string }) {
|
||||
const allArticles = await getAllKnowledgeBaseArticles({
|
||||
knowledgeBaseSlug: kb.slug,
|
||||
language,
|
||||
});
|
||||
|
||||
const existing = allArticles.find((p) => p.id === articleId);
|
||||
if (!existing) {
|
||||
throw new KnowledgeBaseServiceError("invalid-article");
|
||||
}
|
||||
|
||||
const { slug, maxOrder, number } = KnowledgeBaseUtils.getAvailableArticleSlug({
|
||||
allArticles,
|
||||
initialSlug: existing.slug,
|
||||
});
|
||||
|
||||
const item = await createKnowledgeBaseArticle({
|
||||
knowledgeBaseId: kb.id,
|
||||
categoryId: existing.categoryId,
|
||||
sectionId: existing.sectionId,
|
||||
slug,
|
||||
title: existing.title + " " + number,
|
||||
description: existing.description,
|
||||
order: maxOrder + 1,
|
||||
contentDraft: existing.contentDraft,
|
||||
contentPublished: existing.contentPublished,
|
||||
contentPublishedAsText: existing.contentPublishedAsText,
|
||||
contentType: existing.contentType,
|
||||
language: existing.language,
|
||||
featuredOrder: existing.featuredOrder,
|
||||
createdByUserId: existing.createdByUserId,
|
||||
seoImage: existing.seoImage,
|
||||
publishedAt: null,
|
||||
});
|
||||
return item;
|
||||
}
|
||||
|
||||
async function search({
|
||||
query,
|
||||
kb,
|
||||
params,
|
||||
request,
|
||||
}: {
|
||||
query: string | undefined;
|
||||
kb: KnowledgeBaseDto;
|
||||
params: { lang?: string };
|
||||
request: Request;
|
||||
}): Promise<KbSearchResultDto | undefined> {
|
||||
if (!query) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Hot-path: routes the read through @toolctrl/search's tenant-scoped
|
||||
// wrapper when MEILI_URL is set; transparently falls back to the SQL
|
||||
// LIKE-based implementation on connectivity errors or when no
|
||||
// Meilisearch backend is configured. See docs/features/search.md.
|
||||
const kbWithTenant = (await getKnowledgeBaseById(kb.id)) as { tenantId?: string | null } | null;
|
||||
const articles = await searchKnowledgeBaseArticles({
|
||||
knowledgeBaseSlug: kb.slug,
|
||||
language: params.lang || kb.defaultLanguage,
|
||||
query,
|
||||
tenantId: kbWithTenant?.tenantId ?? undefined,
|
||||
});
|
||||
const result: KbSearchResultDto = {
|
||||
articles: articles.map((article) => articleToDto({ kb, article, relatedArticles: [], params, request })),
|
||||
query,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
async function newArticle({
|
||||
kb,
|
||||
title,
|
||||
categoryId,
|
||||
sectionId,
|
||||
initialSlug = "untitled",
|
||||
params,
|
||||
userId,
|
||||
position,
|
||||
}: {
|
||||
kb: KnowledgeBaseDto;
|
||||
title?: string;
|
||||
categoryId?: string | null;
|
||||
sectionId?: string | null;
|
||||
initialSlug?: string;
|
||||
params: { lang?: string };
|
||||
userId: string;
|
||||
position: "first" | "last";
|
||||
}) {
|
||||
const language = params.lang || kb.defaultLanguage;
|
||||
const allArticles = await getAllKnowledgeBaseArticles({
|
||||
knowledgeBaseSlug: kb.slug,
|
||||
language,
|
||||
});
|
||||
if (categoryId || sectionId) {
|
||||
const allCategories = await getAllKnowledgeBaseCategories({
|
||||
knowledgeBaseSlug: kb.slug,
|
||||
language,
|
||||
});
|
||||
const category = categoryId ? allCategories.find((item) => item.id === categoryId) : null;
|
||||
if (categoryId && !category) {
|
||||
throw new KnowledgeBaseServiceError("invalid-category");
|
||||
}
|
||||
if (sectionId && !category?.sections.some((section) => section.id === sectionId)) {
|
||||
throw new KnowledgeBaseServiceError("invalid-section");
|
||||
}
|
||||
}
|
||||
const { slug, maxOrder, firstOrder, number } = KnowledgeBaseUtils.getAvailableArticleSlug({
|
||||
allArticles,
|
||||
initialSlug,
|
||||
});
|
||||
const created = await createKnowledgeBaseArticle({
|
||||
knowledgeBaseId: kb.id,
|
||||
categoryId: categoryId ?? null,
|
||||
sectionId: sectionId ?? null,
|
||||
slug,
|
||||
title: title ?? (await getUntitledArticleTitle({ language, number })),
|
||||
description: "",
|
||||
order: position === "first" ? firstOrder - 1 : maxOrder + 1,
|
||||
contentDraft: "",
|
||||
contentPublished: "",
|
||||
contentPublishedAsText: "",
|
||||
contentType: "wysiwyg",
|
||||
language,
|
||||
featuredOrder: null,
|
||||
createdByUserId: userId,
|
||||
seoImage: "",
|
||||
publishedAt: null,
|
||||
});
|
||||
return created;
|
||||
}
|
||||
|
||||
async function getUntitledArticleTitle({ language, number }: { language: string | undefined; number: number }) {
|
||||
const locale = language && isSupportedLocale(language) ? language : defaultLocale;
|
||||
const t = await remixI18Next.getFixedT(locale, "translations");
|
||||
return t("knowledgeBase.adminCategories.untitledNumbered", { number }) as string;
|
||||
}
|
||||
|
||||
function contentAsText(text: string) {
|
||||
return htmlToText(marked.parse(text, { async: false }) as string, {
|
||||
wordwrap: 130,
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
getAll,
|
||||
get,
|
||||
getById,
|
||||
getCategories,
|
||||
getArticles,
|
||||
getArticle,
|
||||
getArticleById,
|
||||
getFeaturedArticles,
|
||||
getCategory,
|
||||
del,
|
||||
duplicateCategory,
|
||||
duplicateArticle,
|
||||
search,
|
||||
newArticle,
|
||||
contentAsText,
|
||||
};
|
||||
Reference in new issue
Block a user