-
{
- if (e.key === 'Enter' || e.key === ' ') {
- handleUploadClick();
+ if (selectedNodeIds.length === 0) {
+ toast.error(t('publishBase.tips.atLeastOneNode'));
+ return;
}
+
+ publishBaseMutate({ title, description: description || '' });
}}
+ disabled={publishBaseLoading}
>
- {screenshotUrl ? (
- <>
-

-
-
-
- {t('publishBase.changeCover')}
+
+ {templateDetail ? t('publishBase.update') : t('publishBase.publish')}
+
+ {publishBaseLoading && }
+
+
+
+
+
+
+
+
{t('publishBase.previewTips')}
+
+
+
{
+ if (e.key === 'Enter' || e.key === ' ') {
+ handleUploadClick();
+ }
+ }}
+ >
+ {screenshotUrl ? (
+ <>
+

+
+
+
+
+ {t('publishBase.changeCover')}
+
+
+ >
+ ) : (
+
+ {isUploading ? (
+ <>
+
+
{uploadProgress}%
+ >
+ ) : (
+ <>
+
+
+ {t('publishBase.uploadCover')}
+
+ >
+ )}
- >
- ) : (
-
- {isUploading ? (
- <>
-
-
{uploadProgress}%
- >
- ) : (
- <>
-
-
- {t('publishBase.uploadCover')}
-
- >
- )}
-
- )}
-
-
-
- {title || t('publishBase.form.toBeFilled')}
-
-
-
-
-
- {user?.name?.slice(0, 1)}
-
-
{user?.name}
-
+
+
+
+
+ {title || t('publishBase.form.toBeFilled')}
+
-
-
- {t('publishBase.usageCount')}
- {templateDetail?.usageCount || 0}
+
+
+ {templateDetail?.usageCount || 0}
+
+
+ {description || t('publishBase.form.toBeFilled')}
+
-
- {description || t('publishBase.form.toBeFilled')}
-
-
-
-
+
+
+
+
+ >
);
};
diff --git a/apps/nextjs-app/src/features/app/components/space/template/CategoryMenu.tsx b/apps/nextjs-app/src/features/app/components/space/template/CategoryMenu.tsx
index 6d30e296c5..47982a1775 100644
--- a/apps/nextjs-app/src/features/app/components/space/template/CategoryMenu.tsx
+++ b/apps/nextjs-app/src/features/app/components/space/template/CategoryMenu.tsx
@@ -1,5 +1,4 @@
import { useQuery } from '@tanstack/react-query';
-import type { ITemplateCategoryListVo } from '@teable/openapi';
import { getPublishedTemplateCategoryList } from '@teable/openapi';
import { ReactQueryKeys } from '@teable/sdk/config';
import { useIsMobile } from '@teable/sdk/hooks';
@@ -16,9 +15,9 @@ interface ICategoryMenuProps {
onCategoryChange: (category: string | null) => void;
className?: string;
categoryHeaderRender?: () => React.ReactNode;
- serverPublishedTemplateCategoryList?: ITemplateCategoryListVo[];
- isFeatured: boolean;
- onFeaturedChange: (isFeatured: boolean) => void;
+ isFeatured: boolean | undefined;
+ onFeaturedChange: (isFeatured: boolean | undefined) => void;
+ disabledFeaturedToggle: boolean;
}
export const CategoryMenu = (props: ICategoryMenuProps) => {
@@ -27,15 +26,14 @@ export const CategoryMenu = (props: ICategoryMenuProps) => {
onCategoryChange,
className,
categoryHeaderRender,
- serverPublishedTemplateCategoryList,
onFeaturedChange,
isFeatured,
+ disabledFeaturedToggle,
} = props;
const { t } = useTranslation('common');
const { data: categoryList } = useQuery({
queryKey: ReactQueryKeys.publishedTemplateCategoryList(),
queryFn: () => getPublishedTemplateCategoryList().then((data) => data.data),
- initialData: serverPublishedTemplateCategoryList,
});
const isMobile = useIsMobile();
@@ -54,8 +52,15 @@ export const CategoryMenu = (props: ICategoryMenuProps) => {
)}
{
+ if (pressed) {
+ onFeaturedChange(true);
+ } else {
+ onFeaturedChange(undefined);
+ }
+ }}
+ disabled={disabledFeaturedToggle}
>
{t('settings.templateAdmin.category.menu.recommended')}
diff --git a/apps/nextjs-app/src/features/app/components/space/template/TemplateCard.tsx b/apps/nextjs-app/src/features/app/components/space/template/TemplateCard.tsx
index 7838213879..4cadd1571b 100644
--- a/apps/nextjs-app/src/features/app/components/space/template/TemplateCard.tsx
+++ b/apps/nextjs-app/src/features/app/components/space/template/TemplateCard.tsx
@@ -1,4 +1,4 @@
-import { ArrowRight } from '@teable/icons';
+import { ArrowRight, Heart } from '@teable/icons';
import type { ITemplateVo } from '@teable/openapi';
import { useTranslation } from 'next-i18next';
import { useBrand } from '@/features/app/hooks/useBrand';
@@ -16,7 +16,7 @@ export const TemplateCard = ({ template, onClickTemplateCardHandler }: ITemplate
return (
{
@@ -41,28 +41,19 @@ export const TemplateCard = ({ template, onClickTemplateCardHandler }: ITemplate
-
-
+
+
{name}
-
-
-
+
+
+
+ {usageCount > 999 ? '999+' : usageCount}
+
-
+
{description}
-
-
- {t('settings.templateAdmin.createdBy', { user: brandName })}
-
-
- {t('settings.templateAdmin.usageCount', { count: usageCount })}
-
-
);
diff --git a/apps/nextjs-app/src/features/app/components/space/template/TemplateList.tsx b/apps/nextjs-app/src/features/app/components/space/template/TemplateList.tsx
index 794b75caa9..585f991053 100644
--- a/apps/nextjs-app/src/features/app/components/space/template/TemplateList.tsx
+++ b/apps/nextjs-app/src/features/app/components/space/template/TemplateList.tsx
@@ -13,11 +13,10 @@ interface ITemplateListProps extends ITemplateBaseProps {
currentCategoryId: string | null;
search: string;
className?: string;
- serverPublishedTemplateList?: ITemplateVo[];
- isFeatured: boolean;
+ isFeatured: boolean | undefined;
}
-const PAGE_SIZE = 10;
+const PAGE_SIZE = 2 * 3 * 2;
export const TemplateList = (props: ITemplateListProps) => {
const {
@@ -26,7 +25,6 @@ export const TemplateList = (props: ITemplateListProps) => {
onClickUseTemplateHandler,
onClickTemplateCardHandler,
className,
- serverPublishedTemplateList,
isFeatured,
} = props;
const { t } = useTranslation(['common']);
@@ -47,16 +45,10 @@ export const TemplateList = (props: ITemplateListProps) => {
}
return allPages.length * PAGE_SIZE;
},
- initialData: serverPublishedTemplateList
- ? {
- pages: [serverPublishedTemplateList],
- pageParams: [undefined],
- }
- : undefined,
});
const currentTemplateList = useMemo(() => {
- return data?.pages.flatMap((page) => page) ?? [];
+ return data?.pages?.flatMap((page) => page) ?? [];
}, [data]);
return (
diff --git a/apps/nextjs-app/src/features/app/components/space/template/TemplateMain.tsx b/apps/nextjs-app/src/features/app/components/space/template/TemplateMain.tsx
index 593bbf4150..198f7734ce 100644
--- a/apps/nextjs-app/src/features/app/components/space/template/TemplateMain.tsx
+++ b/apps/nextjs-app/src/features/app/components/space/template/TemplateMain.tsx
@@ -18,10 +18,7 @@ interface ITemplateMainProps extends ITemplateBaseProps {
categoryHeaderRender?: () => React.ReactNode;
className?: string;
templateListClassName?: string;
- serverData?: {
- publishedTemplateList?: ITemplateVo[];
- publishedTemplateCategoryList?: ITemplateCategoryListVo[];
- };
+ disabledFeaturedToggle?: boolean;
}
export const TemplateMain = (props: ITemplateMainProps) => {
@@ -36,10 +33,9 @@ export const TemplateMain = (props: ITemplateMainProps) => {
categoryHeaderRender,
className,
templateListClassName,
- serverData,
+ disabledFeaturedToggle = true,
} = props;
- const [isFeatured, setIsFeatured] = useState(true);
- const { publishedTemplateList, publishedTemplateCategoryList } = serverData || {};
+ const [isFeatured, setIsFeatured] = useState
(true);
return (
{
onCategoryChange={onCategoryChange}
className={categoryMenuClassName}
categoryHeaderRender={categoryHeaderRender}
- serverPublishedTemplateCategoryList={publishedTemplateCategoryList}
isFeatured={isFeatured}
onFeaturedChange={setIsFeatured}
+ disabledFeaturedToggle={disabledFeaturedToggle}
/>
{
onClickUseTemplateHandler={onClickUseTemplateHandler}
onClickTemplateCardHandler={onClickTemplateCardHandler}
className={templateListClassName}
- serverPublishedTemplateList={publishedTemplateList}
isFeatured={isFeatured}
/>
diff --git a/packages/common-i18n/src/locales/en/space.json b/packages/common-i18n/src/locales/en/space.json
index 39a9609424..420e3754f8 100644
--- a/packages/common-i18n/src/locales/en/space.json
+++ b/packages/common-i18n/src/locales/en/space.json
@@ -134,7 +134,7 @@
},
"publishToCommunity": "Publish to community",
"publish": "Publish",
- "publishSuccess": "Base published successfully!",
+ "publishSuccess": "Publish success!",
"previewTips": "Let the world see your work",
"update": "Update",
"unPublish": "Unpublish",
@@ -150,6 +150,10 @@
"tips": {
"publishValidation": "title and description are required",
"atLeastOneNode": "At least select one node to publish"
- }
+ },
+ "urlCopied": "URL copied to clipboard!",
+ "urlCopiedForDiscord": "URL copied to clipboard! You can paste it in Discord.",
+ "publishSuccessDescription": "Share your work to the world",
+ "shareWith": "Share with"
}
}
diff --git a/packages/common-i18n/src/locales/zh/space.json b/packages/common-i18n/src/locales/zh/space.json
index 357e1c4633..9f18c7bf39 100644
--- a/packages/common-i18n/src/locales/zh/space.json
+++ b/packages/common-i18n/src/locales/zh/space.json
@@ -150,6 +150,10 @@
"tips": {
"publishValidation": "标题和描述不能为空",
"atLeastOneNode": "至少选择一个节点发布"
- }
+ },
+ "urlCopied": "链接已复制到剪贴板!",
+ "urlCopiedForDiscord": "链接已复制到剪贴板!你可以在 Discord 中粘贴。",
+ "publishSuccessDescription": "分享你的作品到世界",
+ "shareWith": "分享到"
}
}
diff --git a/packages/db-main-prisma/prisma/postgres/migrations/20251208112242_template_community/migration.sql b/packages/db-main-prisma/prisma/postgres/migrations/20251208112242_template_community/migration.sql
index 223ebfaf88..3271a7a1be 100644
--- a/packages/db-main-prisma/prisma/postgres/migrations/20251208112242_template_community/migration.sql
+++ b/packages/db-main-prisma/prisma/postgres/migrations/20251208112242_template_community/migration.sql
@@ -20,5 +20,7 @@ ALTER TABLE "template" ADD COLUMN "publish_info" JSONB;
-- CreateIndex
CREATE UNIQUE INDEX "template_base_id_key" ON "template"("base_id");
+UPDATE "template" SET featured = true WHERE is_published = true;
+
COMMIT;
diff --git a/packages/db-main-prisma/prisma/sqlite/migrations/20251208112242_template_community/migration.sql b/packages/db-main-prisma/prisma/sqlite/migrations/20251208112242_template_community/migration.sql
index 0c2b99a12c..bd39af7c47 100644
--- a/packages/db-main-prisma/prisma/sqlite/migrations/20251208112242_template_community/migration.sql
+++ b/packages/db-main-prisma/prisma/sqlite/migrations/20251208112242_template_community/migration.sql
@@ -8,4 +8,6 @@ ALTER TABLE "template" ADD COLUMN "publish_info" JSON;
-- CreateIndex
CREATE UNIQUE INDEX "template_base_id_key" ON "template"("base_id");
+UPDATE "template" SET featured = true WHERE is_published = true;
+
COMMIT;
\ No newline at end of file
diff --git a/packages/icons/src/components/Discord.tsx b/packages/icons/src/components/Discord.tsx
new file mode 100644
index 0000000000..833129ceee
--- /dev/null
+++ b/packages/icons/src/components/Discord.tsx
@@ -0,0 +1,15 @@
+import * as React from 'react';
+import type { SVGProps } from 'react';
+const Discord = (props: SVGProps) => (
+
+);
+export default Discord;
diff --git a/packages/icons/src/components/InIcon.tsx b/packages/icons/src/components/InIcon.tsx
new file mode 100644
index 0000000000..b986aa3e1a
--- /dev/null
+++ b/packages/icons/src/components/InIcon.tsx
@@ -0,0 +1,15 @@
+import * as React from 'react';
+import type { SVGProps } from 'react';
+const InIcon = (props: SVGProps) => (
+
+);
+export default InIcon;
diff --git a/packages/icons/src/components/Twitter.tsx b/packages/icons/src/components/Twitter.tsx
new file mode 100644
index 0000000000..e7e16bc936
--- /dev/null
+++ b/packages/icons/src/components/Twitter.tsx
@@ -0,0 +1,15 @@
+import * as React from 'react';
+import type { SVGProps } from 'react';
+const Twitter = (props: SVGProps) => (
+
+);
+export default Twitter;
diff --git a/packages/icons/src/index.ts b/packages/icons/src/index.ts
index 9cf273e1be..d196e9bd14 100644
--- a/packages/icons/src/index.ts
+++ b/packages/icons/src/index.ts
@@ -197,3 +197,6 @@ export { default as AmazonBedrock } from './components/AmazonBedrock';
export { default as TeableAi } from './components/TeableAi';
export { default as Compose } from './components/Compose';
export { default as MousePointerClick } from './components/MousePointerClick';
+export { default as InIcon } from './components/InIcon';
+export { default as Discord } from './components/Discord';
+export { default as Twitter } from './components/Twitter';
diff --git a/packages/openapi/src/base/publish.ts b/packages/openapi/src/base/publish.ts
index 27e5b1a3b7..9cee91e8f7 100644
--- a/packages/openapi/src/base/publish.ts
+++ b/packages/openapi/src/base/publish.ts
@@ -43,7 +43,7 @@ export const PublishBaseRoute: RouteConfig = registerRoute({
});
export const publishBase = async (baseId: string, publishBaseRo: IPublishBaseRo) => {
- return await axios.post(
+ return await axios.post<{ baseId: string }>(
urlBuilder(PUBLISH_BASE, {
baseId,
}),
diff --git a/packages/sdk/src/config/react-query-keys.ts b/packages/sdk/src/config/react-query-keys.ts
index f63f2c30f8..348002fe54 100644
--- a/packages/sdk/src/config/react-query-keys.ts
+++ b/packages/sdk/src/config/react-query-keys.ts
@@ -43,11 +43,15 @@ export const ReactQueryKeys = {
publishedTemplateCategoryList: () => ['published-template-category-list'] as const,
- publishedTemplateList: (categoryId?: string | null, search?: string, isFeatured?: boolean) => {
+ publishedTemplateList: (categoryId: string | null, search: string, isFeatured?: boolean) => {
const parts: (string | null)[] = ['published-template-list'];
if (categoryId !== undefined) parts.push(categoryId);
if (search !== undefined) parts.push(search);
- if (isFeatured !== undefined) parts.push(isFeatured ? 'true' : 'false');
+ if (isFeatured !== undefined) {
+ parts.push(isFeatured.toString());
+ } else {
+ parts.push('undefined');
+ }
return parts;
},