diff --git a/apps/nestjs-backend/src/features/base/base.service.ts b/apps/nestjs-backend/src/features/base/base.service.ts index 0453a818b4..d32c9ba47c 100644 --- a/apps/nestjs-backend/src/features/base/base.service.ts +++ b/apps/nestjs-backend/src/features/base/base.service.ts @@ -616,12 +616,11 @@ export class BaseService { where: { baseId }, select: { id: true }, }); + const { title, description, cover, nodes, includeData } = publishBaseRo; + const snapshot = await this.createSnapshot(baseId, nodes, includeData); // if already published, update template if (template) { - const { title, description, cover, nodes, includeData } = publishBaseRo; - const snapshot = await this.createSnapshot(baseId, nodes, includeData); - await prisma.template.update({ where: { id: template.id }, data: { @@ -642,14 +641,18 @@ export class BaseService { }, }, }); - return; + return { + baseId: snapshot.baseId, + }; } // if the base is not published, create a template - const { nodes, includeData } = publishBaseRo; - const snapshot = await this.createSnapshot(baseId, nodes, includeData); // publish snapshot await this.createTemplateBySnapshot(baseId, snapshot, publishBaseRo); + + return { + baseId: snapshot.baseId, + }; } private async createSnapshot(baseId: string, nodes?: string[], includeData?: boolean) { diff --git a/apps/nestjs-backend/src/types/i18n.generated.ts b/apps/nestjs-backend/src/types/i18n.generated.ts index ef7eeea73d..316b0592b2 100644 --- a/apps/nestjs-backend/src/types/i18n.generated.ts +++ b/apps/nestjs-backend/src/types/i18n.generated.ts @@ -2813,6 +2813,10 @@ export type I18nTranslations = { "publishValidation": string; "atLeastOneNode": string; }; + "urlCopied": string; + "urlCopiedForDiscord": string; + "publishSuccessDescription": string; + "shareWith": string; }; }; "system": { diff --git a/apps/nextjs-app/src/features/app/blocks/table/table-header/publish-base/NodeTreeSelect.tsx b/apps/nextjs-app/src/features/app/blocks/table/table-header/publish-base/NodeTreeSelect.tsx index 201ecfbb7a..39cfabbefd 100644 --- a/apps/nextjs-app/src/features/app/blocks/table/table-header/publish-base/NodeTreeSelect.tsx +++ b/apps/nextjs-app/src/features/app/blocks/table/table-header/publish-base/NodeTreeSelect.tsx @@ -448,15 +448,13 @@ export const NodeTreeSelect = (props: INodeSelectProps) => { paddingLeft: `${item.getItemMeta().level * INDENTATION_WIDTH + 8}px`, }} > - {isFolder ? ( + {isFolder && ( - ) : ( -
)} {renderNodeIcon(item)} diff --git a/apps/nextjs-app/src/features/app/blocks/table/table-header/publish-base/PublishBaseDialog.tsx b/apps/nextjs-app/src/features/app/blocks/table/table-header/publish-base/PublishBaseDialog.tsx index 346fc6de62..3481c7ac6a 100644 --- a/apps/nextjs-app/src/features/app/blocks/table/table-header/publish-base/PublishBaseDialog.tsx +++ b/apps/nextjs-app/src/features/app/blocks/table/table-header/publish-base/PublishBaseDialog.tsx @@ -1,6 +1,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { generateAttachmentId } from '@teable/core'; -import { Plus } from '@teable/icons'; +import { Discord, Heart, InIcon, Link, Plus, Twitter } from '@teable/icons'; import type { ITemplateCoverRo, INotifyVo } from '@teable/openapi'; import { getTemplateByBaseId, @@ -10,7 +10,7 @@ import { BaseNodeResourceType, } from '@teable/openapi'; import { AttachmentManager } from '@teable/sdk/components'; -import { useBase, useSession } from '@teable/sdk/hooks'; +import { useBase } from '@teable/sdk/hooks'; import { Spin } from '@teable/ui-lib'; import { AlertDialog, @@ -22,9 +22,6 @@ import { AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, - Avatar, - AvatarFallback, - AvatarImage, Button, cn, Dialog, @@ -39,9 +36,10 @@ import { Textarea, } from '@teable/ui-lib/shadcn'; import { toast } from '@teable/ui-lib/shadcn/ui/sonner'; -import { Camera, Send, SmilePlus } from 'lucide-react'; +import { Camera, Send, Copy, ExternalLink } from 'lucide-react'; import { useTranslation } from 'next-i18next'; import { useState, useRef, useEffect, useMemo } from 'react'; +import { useIsCloud } from '@/features/app/hooks/useIsCloud'; import { ROOT_ID } from '../../../base/base-node/hooks'; import { useBaseNodeContext } from '../../../base/base-node/hooks/useBaseNodeContext'; import { NodeSelect } from './NodeSelect'; @@ -59,6 +57,7 @@ export const PublishBaseDialog = (props: IPublishBaseDialogProps) => { const base = useBase(); const baseId = base?.id; const { treeItems } = useBaseNodeContext(); + const isCloud = useIsCloud(); const queryClient = useQueryClient(); @@ -149,11 +148,17 @@ export const PublishBaseDialog = (props: IPublishBaseDialogProps) => { defaultActiveNodeId, }).then((res) => res.data); }, - onSuccess: () => { - toast.success(t('publishBase.publishSuccess')); + onSuccess: (data) => { + const { baseId: templateBaseId } = data; queryClient.invalidateQueries({ queryKey: ['template-by-base', baseId] }); // after publish success, clear the uploaded cover, use server data next time setUploadedCover(null); + // Close the publish dialog and show success dialog + setOpen(false); + // Generate share URL based on baseId + const origin = typeof window !== 'undefined' ? window.location.origin : ''; + setShareUrl(`${origin}/base/${templateBaseId}`); + setSuccessDialogOpen(true); }, }); @@ -172,9 +177,10 @@ export const PublishBaseDialog = (props: IPublishBaseDialogProps) => { >(null); const [includeData, setIncludeData] = useState(false); const [defaultActiveNodeId, setDefaultActiveNodeId] = useState(null); - const { user } = useSession(); const uploadRef = useRef(null); const [hasLoadedTemplate, setHasLoadedTemplate] = useState(false); + const [successDialogOpen, setSuccessDialogOpen] = useState(false); + const [shareUrl, setShareUrl] = useState(''); // Initialize selected nodes on first load useEffect(() => { @@ -270,222 +276,314 @@ export const PublishBaseDialog = (props: IPublishBaseDialogProps) => { } }, [selectedNodeIds, defaultActiveNodeId]); + const handleCopyUrl = () => { + navigator.clipboard.writeText(shareUrl); + toast.success(t('publishBase.urlCopied')); + }; + + const handleOpenUrl = () => { + window.open(shareUrl, '_blank'); + }; + + const handleShareToX = () => { + const text = encodeURIComponent(`Check out this template: ${title}`); + window.open( + `https://twitter.com/intent/tweet?text=${text}&url=${encodeURIComponent(shareUrl)}`, + '_blank' + ); + }; + + const handleShareToLinkedIn = () => { + window.open( + `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`, + '_blank' + ); + }; + + const handleShareToDiscord = () => { + // Discord doesn't have a direct share URL, so we just copy the URL + navigator.clipboard.writeText(shareUrl); + toast.success(t('publishBase.urlCopiedForDiscord')); + }; + return ( - - {children} - - - {t('publishBase.title')} - - {t('publishBase.description')} - - -
-
-
-
{t('publishBase.infoTitle')}
+ <> + + {children} + + + {t('publishBase.title')} + + {t('publishBase.description')} + + +
+
- {t('publishBase.form.title')} - setTitle(e.target.value)} - placeholder={t('publishBase.form.titlePlaceholder')} - /> +
{t('publishBase.infoTitle')}
+
+ {t('publishBase.form.title')} + setTitle(e.target.value)} + placeholder={t('publishBase.form.titlePlaceholder')} + /> +
+ +
+ {t('publishBase.form.description')} +