649 lines
21 KiB
TypeScript
649 lines
21 KiB
TypeScript
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,
|
|
};
|