From 3fa72106e9017b0f85977d5fa00b2cd120aef0d8 Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:28:46 +0800 Subject: [PATCH] fix: upload template (#6021) * fix: upload template * fix: scale thumb --- frontend/appflowy_web_app/deploy/server.ts | 2 +- frontend/appflowy_web_app/package.json | 2 + frontend/appflowy_web_app/pnpm-lock.yaml | 33 +- .../src/application/publish/context.tsx | 16 +- .../services/js-services/http/http_api.ts | 296 ++++++++++++++++-- .../services/js-services/http/utils.ts | 2 +- .../application/services/js-services/index.ts | 111 +++++-- .../src/application/services/services.type.ts | 28 +- .../services/tauri-services/index.ts | 104 ++++-- .../application/slate-yjs/plugins/withYjs.ts | 27 +- .../src/application/template.type.ts | 83 +++++ frontend/appflowy_web_app/src/assets/add.svg | 4 + frontend/appflowy_web_app/src/assets/book.svg | 7 + .../src/assets/chat_circle_text.svg | 7 + .../appflowy_web_app/src/assets/cloud_add.svg | 12 + .../appflowy_web_app/src/assets/columns.svg | 12 + .../src/assets/currency_circle_dollar.svg | 7 + .../appflowy_web_app/src/assets/database.svg | 7 + frontend/appflowy_web_app/src/assets/edit.svg | 5 + .../appflowy_web_app/src/assets/facebook.svg | 11 + .../src/assets/graduation_cap.svg | 7 + .../appflowy_web_app/src/assets/inbox.svg | 7 + .../appflowy_web_app/src/assets/instagram.svg | 11 + .../appflowy_web_app/src/assets/lightbulb.svg | 7 + .../appflowy_web_app/src/assets/lightning.svg | 13 + .../appflowy_web_app/src/assets/linkedin.svg | 12 + .../src/assets/megaphone_simple.svg | 7 + .../appflowy_web_app/src/assets/monitor.svg | 7 + .../appflowy_web_app/src/assets/notepad.svg | 7 + .../appflowy_web_app/src/assets/person.svg | 7 + .../appflowy_web_app/src/assets/sparkle.svg | 21 ++ .../appflowy_web_app/src/assets/template.svg | 6 + .../appflowy_web_app/src/assets/tiktok.svg | 5 + .../appflowy_web_app/src/assets/trash.svg | 2 +- .../appflowy_web_app/src/assets/twitter.svg | 6 + .../src/assets/users_three.svg | 7 + .../appflowy_web_app/src/assets/website.svg | 6 + .../appflowy_web_app/src/assets/youtube.svg | 9 + .../_shared/file-dropzone/FileDropzone.tsx | 100 ++++++ .../components/_shared/modal/NormalModal.tsx | 17 +- .../_shared/popover/RichTooltip.tsx | 4 +- .../components => _shared}/tabs/ViewTabs.tsx | 0 .../src/components/app/App.tsx | 2 + .../src/components/app/AppConfig.tsx | 53 ++-- .../src/components/app/AppTheme.tsx | 17 +- .../src/components/app/app.hooks.ts | 45 +++ .../src/components/app/useAppThemeMode.ts | 23 +- .../src/components/as-template/AsTemplate.tsx | 191 +++++++++++ .../as-template/AsTemplateButton.tsx | 40 +++ .../components/as-template/AsTemplateForm.tsx | 122 ++++++++ .../components/as-template/DeleteTemplate.tsx | 41 +++ .../as-template/category/AddCategory.tsx | 68 ++++ .../as-template/category/BgColorPicker.tsx | 53 ++++ .../as-template/category/Categories.tsx | 122 ++++++++ .../as-template/category/CategoryForm.tsx | 149 +++++++++ .../as-template/category/CategoryItem.tsx | 90 ++++++ .../as-template/category/DeleteCategory.tsx | 41 +++ .../as-template/category/EditCategory.tsx | 53 ++++ .../as-template/category/IconPicker.tsx | 38 +++ .../as-template/creator/AccountLinks.tsx | 62 ++++ .../as-template/creator/AddCreator.tsx | 66 ++++ .../as-template/creator/Creator.tsx | 128 ++++++++ .../as-template/creator/CreatorAvatar.tsx | 124 ++++++++ .../as-template/creator/CreatorForm.tsx | 102 ++++++ .../as-template/creator/CreatorItem.tsx | 88 ++++++ .../as-template/creator/DeleteCreator.tsx | 41 +++ .../as-template/creator/EditCreator.tsx | 51 +++ .../as-template/creator/UploadAvatar.tsx | 104 ++++++ .../src/components/as-template/hooks.ts | 124 ++++++++ .../src/components/as-template/icons.tsx | 62 ++++ .../src/components/as-template/index.ts | 3 + .../related-template/AddRelatedTemplates.tsx | 65 ++++ .../related-template/CategoryTemplateItem.tsx | 76 +++++ .../related-template/CategoryTemplates.tsx | 124 ++++++++ .../related-template/RelatedTemplates.tsx | 75 +++++ .../related-template/TemplateItem.tsx | 45 +++ .../src/components/as-template/template.scss | 45 +++ .../src/components/database/Database.tsx | 4 +- .../database/calendar/calendar.scss | 8 + .../database/components/tabs/DatabaseTabs.tsx | 12 +- .../database/components/tabs/index.ts | 2 +- .../src/components/document/Document.tsx | 13 +- .../components/editor/CollaborativeEditor.tsx | 10 +- .../src/components/editor/EditorContext.tsx | 3 +- .../global-comment/GlobalComment.hooks.tsx | 23 +- .../global-comment/ReplyComment.tsx | 4 +- .../global-comment/actions/MoreActions.tsx | 2 +- .../global-comment/actions/ReactAction.tsx | 2 +- .../global-comment/add-comment/AddComment.tsx | 2 +- .../add-comment/AddCommentWrapper.tsx | 4 +- .../global-comment/comment/Comment.tsx | 4 +- .../global-comment/comment/CommentWrap.tsx | 2 +- .../global-comment/reactions/Reaction.tsx | 2 +- .../src/components/login/LoginAuth.tsx | 2 +- .../src/components/login/LoginProvider.tsx | 2 +- .../src/components/login/MagicLink.tsx | 2 +- .../src/components/publish/CollabView.tsx | 9 +- .../src/components/publish/DatabaseView.tsx | 11 +- .../src/components/publish/PublishView.tsx | 49 +-- .../components/publish/header/MoreActions.tsx | 32 +- .../publish/header/PublishViewHeader.tsx | 24 +- .../header/duplicate/DuplicateModal.tsx | 2 +- .../header/duplicate/SelectWorkspace.tsx | 2 +- .../publish/header/duplicate/useDuplicate.ts | 2 +- .../src/components/publish/useViewMeta.ts | 6 +- .../src/components/view-meta/ViewCover.tsx | 4 +- .../components/view-meta/ViewMetaPreview.tsx | 8 +- .../src/pages/AsTemplatePage.tsx | 29 ++ .../appflowy_web_app/src/pages/LoginPage.tsx | 2 +- frontend/appflowy_web_app/src/styles/app.scss | 4 +- frontend/appflowy_web_app/src/utils/color.ts | 13 +- .../appflowy_web_app/src/utils/hotkeys.ts | 4 +- frontend/resources/translations/en.json | 61 ++++ 113 files changed, 3697 insertions(+), 245 deletions(-) create mode 100644 frontend/appflowy_web_app/src/application/template.type.ts create mode 100644 frontend/appflowy_web_app/src/assets/add.svg create mode 100644 frontend/appflowy_web_app/src/assets/book.svg create mode 100644 frontend/appflowy_web_app/src/assets/chat_circle_text.svg create mode 100644 frontend/appflowy_web_app/src/assets/cloud_add.svg create mode 100644 frontend/appflowy_web_app/src/assets/columns.svg create mode 100644 frontend/appflowy_web_app/src/assets/currency_circle_dollar.svg create mode 100644 frontend/appflowy_web_app/src/assets/database.svg create mode 100644 frontend/appflowy_web_app/src/assets/edit.svg create mode 100644 frontend/appflowy_web_app/src/assets/facebook.svg create mode 100644 frontend/appflowy_web_app/src/assets/graduation_cap.svg create mode 100644 frontend/appflowy_web_app/src/assets/inbox.svg create mode 100644 frontend/appflowy_web_app/src/assets/instagram.svg create mode 100644 frontend/appflowy_web_app/src/assets/lightbulb.svg create mode 100644 frontend/appflowy_web_app/src/assets/lightning.svg create mode 100644 frontend/appflowy_web_app/src/assets/linkedin.svg create mode 100644 frontend/appflowy_web_app/src/assets/megaphone_simple.svg create mode 100644 frontend/appflowy_web_app/src/assets/monitor.svg create mode 100644 frontend/appflowy_web_app/src/assets/notepad.svg create mode 100644 frontend/appflowy_web_app/src/assets/person.svg create mode 100644 frontend/appflowy_web_app/src/assets/sparkle.svg create mode 100644 frontend/appflowy_web_app/src/assets/template.svg create mode 100644 frontend/appflowy_web_app/src/assets/tiktok.svg create mode 100644 frontend/appflowy_web_app/src/assets/twitter.svg create mode 100644 frontend/appflowy_web_app/src/assets/users_three.svg create mode 100644 frontend/appflowy_web_app/src/assets/website.svg create mode 100644 frontend/appflowy_web_app/src/assets/youtube.svg create mode 100644 frontend/appflowy_web_app/src/components/_shared/file-dropzone/FileDropzone.tsx rename frontend/appflowy_web_app/src/components/{database/components => _shared}/tabs/ViewTabs.tsx (100%) create mode 100644 frontend/appflowy_web_app/src/components/app/app.hooks.ts create mode 100644 frontend/appflowy_web_app/src/components/as-template/AsTemplate.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/AsTemplateButton.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/AsTemplateForm.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/DeleteTemplate.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/category/AddCategory.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/category/BgColorPicker.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/category/Categories.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/category/CategoryForm.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/category/CategoryItem.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/category/DeleteCategory.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/category/EditCategory.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/category/IconPicker.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/creator/AccountLinks.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/creator/AddCreator.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/creator/Creator.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/creator/CreatorAvatar.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/creator/CreatorForm.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/creator/CreatorItem.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/creator/DeleteCreator.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/creator/EditCreator.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/creator/UploadAvatar.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/hooks.ts create mode 100644 frontend/appflowy_web_app/src/components/as-template/icons.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/index.ts create mode 100644 frontend/appflowy_web_app/src/components/as-template/related-template/AddRelatedTemplates.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/related-template/CategoryTemplateItem.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/related-template/CategoryTemplates.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/related-template/RelatedTemplates.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/related-template/TemplateItem.tsx create mode 100644 frontend/appflowy_web_app/src/components/as-template/template.scss create mode 100644 frontend/appflowy_web_app/src/pages/AsTemplatePage.tsx diff --git a/frontend/appflowy_web_app/deploy/server.ts b/frontend/appflowy_web_app/deploy/server.ts index 325268a6b2..43b696139f 100644 --- a/frontend/appflowy_web_app/deploy/server.ts +++ b/frontend/appflowy_web_app/deploy/server.ts @@ -68,7 +68,7 @@ const createServer = async (req: Request) => { logger.info(`Request URL: ${hostname}${reqUrl.pathname}`); - if (['/after-payment', '/login'].includes(reqUrl.pathname)) { + if (['/after-payment', '/login', '/as-template'].includes(reqUrl.pathname)) { timer(); const htmlData = fs.readFileSync(indexPath, 'utf8'); const $ = load(htmlData); diff --git a/frontend/appflowy_web_app/package.json b/frontend/appflowy_web_app/package.json index f397b04388..4e0c87a259 100644 --- a/frontend/appflowy_web_app/package.json +++ b/frontend/appflowy_web_app/package.json @@ -73,6 +73,7 @@ "react-datepicker": "^4.23.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.13", + "react-hook-form": "^7.52.2", "react-hot-toast": "^2.4.1", "react-i18next": "^14.1.0", "react-katex": "^3.0.1", @@ -137,6 +138,7 @@ "@typescript-eslint/parser": "^7.2.0", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.13", + "axios-mock-adapter": "^2.0.0", "babel-jest": "^29.6.2", "chalk": "^4.1.2", "cheerio": "1.0.0-rc.12", diff --git a/frontend/appflowy_web_app/pnpm-lock.yaml b/frontend/appflowy_web_app/pnpm-lock.yaml index 432a5676f0..321468236c 100644 --- a/frontend/appflowy_web_app/pnpm-lock.yaml +++ b/frontend/appflowy_web_app/pnpm-lock.yaml @@ -152,6 +152,9 @@ dependencies: react-error-boundary: specifier: ^4.0.13 version: 4.0.13(react@18.2.0) + react-hook-form: + specifier: ^7.52.2 + version: 7.52.2(react@18.2.0) react-hot-toast: specifier: ^2.4.1 version: 2.4.1(csstype@3.1.3)(react-dom@18.2.0)(react@18.2.0) @@ -340,6 +343,9 @@ devDependencies: autoprefixer: specifier: ^10.4.13 version: 10.4.13(postcss@8.4.21) + axios-mock-adapter: + specifier: ^2.0.0 + version: 2.0.0(axios@1.7.2) babel-jest: specifier: ^29.6.2 version: 29.6.2(@babel/core@7.24.3) @@ -4945,6 +4951,16 @@ packages: /aws4@1.12.0: resolution: {integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==} + /axios-mock-adapter@2.0.0(axios@1.7.2): + resolution: {integrity: sha512-D/K0J5Zm6KvaMTnsWrBQZWLzKN9GxUFZEa0mx2qeEHXDeTugCoplWehy8y36dj5vuSjhe1u/Dol8cZ8lzzmDew==} + peerDependencies: + axios: '>= 0.17.0' + dependencies: + axios: 1.7.2 + fast-deep-equal: 3.1.3 + is-buffer: 2.0.5 + dev: true + /axios@1.7.2: resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==} dependencies: @@ -4953,7 +4969,6 @@ packages: proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - dev: false /b4a@1.6.6: resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==} @@ -6709,7 +6724,6 @@ packages: peerDependenciesMeta: debug: optional: true - dev: false /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -7289,6 +7303,11 @@ packages: resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} dev: false + /is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + dev: true + /is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -9258,7 +9277,6 @@ packages: /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - dev: false /psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} @@ -9512,6 +9530,15 @@ packages: /react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + /react-hook-form@7.52.2(react@18.2.0): + resolution: {integrity: sha512-pqfPEbERnxxiNMPd0bzmt1tuaPcVccywFDpyk2uV5xCIBphHV5T8SVnX9/o3kplPE1zzKt77+YIoq+EMwJp56A==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + dependencies: + react: 18.2.0 + dev: false + /react-hot-toast@2.4.1(csstype@3.1.3)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==} engines: {node: '>=10'} diff --git a/frontend/appflowy_web_app/src/application/publish/context.tsx b/frontend/appflowy_web_app/src/application/publish/context.tsx index 6463f00b25..9e7773624b 100644 --- a/frontend/appflowy_web_app/src/application/publish/context.tsx +++ b/frontend/appflowy_web_app/src/application/publish/context.tsx @@ -1,7 +1,7 @@ import { GetViewRowsMap, LoadView, LoadViewMeta } from '@/application/collab.type'; import { db } from '@/application/db'; import { ViewMeta } from '@/application/db/tables/view_metas'; -import { AFConfigContext } from '@/components/app/AppConfig'; +import { AFConfigContext } from '@/components/app/app.hooks'; import { useLiveQuery } from 'dexie-react-hooks'; import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -9,6 +9,7 @@ import { useNavigate } from 'react-router-dom'; export interface PublishContextType { namespace: string; publishName: string; + isTemplateThumb?: boolean; viewMeta?: ViewMeta; toView: (viewId: string) => Promise; loadViewMeta: LoadViewMeta; @@ -23,10 +24,12 @@ export const PublishProvider = ({ children, namespace, publishName, + isTemplateThumb, }: { children: React.ReactNode; namespace: string; publishName: string; + isTemplateThumb?: boolean; }) => { const viewMeta = useLiveQuery(async () => { const name = `${namespace}_${publishName}`; @@ -87,7 +90,7 @@ export const PublishProvider = ({ return Promise.reject(e); } }, - [navigate, service] + [navigate, service], ); const loadViewMeta = useCallback( @@ -124,7 +127,7 @@ export const PublishProvider = ({ return Promise.reject(e); } }, - [service] + [service], ); const getViewRowsMap = useCallback( @@ -148,7 +151,7 @@ export const PublishProvider = ({ return Promise.reject(e); } }, - [service] + [service], ); const loadView = useCallback( @@ -173,7 +176,7 @@ export const PublishProvider = ({ return Promise.reject(e); } }, - [service] + [service], ); useEffect(() => { @@ -195,6 +198,7 @@ export const PublishProvider = ({ toView, namespace, publishName, + isTemplateThumb, }} > {children} @@ -202,6 +206,6 @@ export const PublishProvider = ({ ); }; -export function usePublishContext() { +export function usePublishContext () { return useContext(PublishContext); } diff --git a/frontend/appflowy_web_app/src/application/services/js-services/http/http_api.ts b/frontend/appflowy_web_app/src/application/services/js-services/http/http_api.ts index 6905d8fd02..b95a382578 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/http/http_api.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/http/http_api.ts @@ -4,6 +4,13 @@ import { initGrantService, refreshToken } from '@/application/services/js-servic import { blobToBytes } from '@/application/services/js-services/http/utils'; import { AFCloudConfig } from '@/application/services/services.type'; import { getTokenParsed, invalidToken } from '@/application/session/token'; +import { + Template, + TemplateCategory, + TemplateCategoryFormValues, + TemplateCreator, TemplateCreatorFormValues, TemplateSummary, + UploadTemplatePayload, +} from '@/application/template.type'; import { FolderView, User, Workspace } from '@/application/types'; import axios, { AxiosInstance } from 'axios'; import dayjs from 'dayjs'; @@ -12,13 +19,16 @@ export * from './gotrue'; let axiosInstance: AxiosInstance | null = null; -export function initAPIService(config: AFCloudConfig) { +export function initAPIService (config: AFCloudConfig) { if (axiosInstance) { return; } axiosInstance = axios.create({ baseURL: config.baseURL, + headers: { + 'Content-Type': 'application/json', + }, }); initGrantService(config.gotrueURL); @@ -27,10 +37,6 @@ export function initAPIService(config: AFCloudConfig) { async (config) => { const token = getTokenParsed(); - Object.assign(config.headers, { - 'Content-Type': 'application/json', - }); - if (!token) { return config; } @@ -56,7 +62,7 @@ export function initAPIService(config: AFCloudConfig) { }, (error) => { return Promise.reject(error); - } + }, ); axiosInstance.interceptors.response.use(async (response) => { @@ -83,7 +89,7 @@ export function initAPIService(config: AFCloudConfig) { }); } -export async function signInWithUrl(url: string) { +export async function signInWithUrl (url: string) { const hash = new URL(url).hash; if (!hash) { @@ -100,7 +106,7 @@ export async function signInWithUrl(url: string) { await refreshToken(refresh_token); } -export async function verifyToken(accessToken: string) { +export async function verifyToken (accessToken: string) { const url = `/api/user/verify/${accessToken}`; const response = await axiosInstance?.get<{ code: number; @@ -119,7 +125,7 @@ export async function verifyToken(accessToken: string) { return Promise.reject(data); } -export async function getCurrentUser(): Promise { +export async function getCurrentUser (): Promise { const url = '/api/user/profile'; const response = await axiosInstance?.get<{ code: number; @@ -155,14 +161,14 @@ export async function getCurrentUser(): Promise { return Promise.reject(data); } -export async function getPublishViewMeta(namespace: string, publishName: string) { +export async function getPublishViewMeta (namespace: string, publishName: string) { const url = `/api/workspace/published/${namespace}/${publishName}`; const response = await axiosInstance?.get(url); return response?.data; } -export async function getPublishViewBlob(namespace: string, publishName: string) { +export async function getPublishViewBlob (namespace: string, publishName: string) { const url = `/api/workspace/published/${namespace}/${publishName}/blob`; const response = await axiosInstance?.get(url, { responseType: 'blob', @@ -171,7 +177,7 @@ export async function getPublishViewBlob(namespace: string, publishName: string) return blobToBytes(response?.data); } -export async function getPublishView(publishNamespace: string, publishName: string) { +export async function getPublishView (publishNamespace: string, publishName: string) { const meta = await getPublishViewMeta(publishNamespace, publishName); const blob = await getPublishViewBlob(publishNamespace, publishName); @@ -207,7 +213,7 @@ export async function getPublishView(publishNamespace: string, publishName: stri } } -export async function getPublishInfoWithViewId(viewId: string) { +export async function getPublishInfoWithViewId (viewId: string) { const url = `/api/workspace/published-info/${viewId}`; const response = await axiosInstance?.get<{ code: number; @@ -227,7 +233,7 @@ export async function getPublishInfoWithViewId(viewId: string) { return Promise.reject(data); } -export async function getPublishViewComments(viewId: string): Promise { +export async function getPublishViewComments (viewId: string): Promise { const url = `/api/workspace/published-info/${viewId}/comment`; const response = await axiosInstance?.get<{ code: number; @@ -276,7 +282,7 @@ export async function getPublishViewComments(viewId: string): Promise> { +export async function getReactions (viewId: string, commentId?: string): Promise> { let url = `/api/workspace/published-info/${viewId}/reaction`; if (commentId) { @@ -327,7 +333,7 @@ export async function getReactions(viewId: string, commentId?: string): Promise< return Promise.reject(data); } -export async function createGlobalCommentOnPublishView(viewId: string, content: string, replyCommentId?: string) { +export async function createGlobalCommentOnPublishView (viewId: string, content: string, replyCommentId?: string) { const url = `/api/workspace/published-info/${viewId}/comment`; const response = await axiosInstance?.post<{ code: number; message: string }>(url, { content, @@ -341,7 +347,7 @@ export async function createGlobalCommentOnPublishView(viewId: string, content: return Promise.reject(response?.data.message); } -export async function deleteGlobalCommentOnPublishView(viewId: string, commentId: string) { +export async function deleteGlobalCommentOnPublishView (viewId: string, commentId: string) { const url = `/api/workspace/published-info/${viewId}/comment`; const response = await axiosInstance?.delete<{ code: number; message: string }>(url, { data: { @@ -356,7 +362,7 @@ export async function deleteGlobalCommentOnPublishView(viewId: string, commentId return Promise.reject(response?.data.message); } -export async function addReaction(viewId: string, commentId: string, reactionType: string) { +export async function addReaction (viewId: string, commentId: string, reactionType: string) { const url = `/api/workspace/published-info/${viewId}/reaction`; const response = await axiosInstance?.post<{ code: number; message: string }>(url, { comment_id: commentId, @@ -370,7 +376,7 @@ export async function addReaction(viewId: string, commentId: string, reactionTyp return Promise.reject(response?.data.message); } -export async function removeReaction(viewId: string, commentId: string, reactionType: string) { +export async function removeReaction (viewId: string, commentId: string, reactionType: string) { const url = `/api/workspace/published-info/${viewId}/reaction`; const response = await axiosInstance?.delete<{ code: number; message: string }>(url, { data: { @@ -386,7 +392,7 @@ export async function removeReaction(viewId: string, commentId: string, reaction return Promise.reject(response?.data.message); } -export async function getWorkspaces(): Promise { +export async function getWorkspaces (): Promise { const query = new URLSearchParams({ include_member_count: 'true', }); @@ -436,7 +442,7 @@ export interface WorkspaceFolder { children: WorkspaceFolder[]; } -function iterateFolder(folder: WorkspaceFolder): FolderView { +function iterateFolder (folder: WorkspaceFolder): FolderView { return { id: folder.view_id, name: folder.name, @@ -450,7 +456,7 @@ function iterateFolder(folder: WorkspaceFolder): FolderView { }; } -export async function getWorkspaceFolder(workspaceId: string): Promise { +export async function getWorkspaceFolder (workspaceId: string): Promise { const url = `/api/workspace/${workspaceId}/folder`; const response = await axiosInstance?.get<{ code: number; @@ -473,7 +479,7 @@ export interface DuplicatePublishViewPayload { dest_view_id: string; } -export async function duplicatePublishView(workspaceId: string, payload: DuplicatePublishViewPayload) { +export async function duplicatePublishView (workspaceId: string, payload: DuplicatePublishViewPayload) { const url = `/api/workspace/${workspaceId}/published-duplicate`; const res = await axiosInstance?.post<{ @@ -487,3 +493,247 @@ export async function duplicatePublishView(workspaceId: string, payload: Duplica return Promise.reject(res?.data.message); } + +export async function createTemplate (template: UploadTemplatePayload) { + const url = '/api/template-center/template'; + const response = await axiosInstance?.post<{ + code: number; + message: string; + }>(url, template); + + if (response?.data.code === 0) { + return; + } + + return Promise.reject(response?.data.message); +} + +export async function updateTemplate (viewId: string, template: UploadTemplatePayload) { + const url = `/api/template-center/template/${viewId}`; + const response = await axiosInstance?.put<{ + code: number; + message: string; + }>(url, template); + + if (response?.data.code === 0) { + return; + } + + return Promise.reject(response?.data.message); +} + +export async function getTemplates ({ + categoryId, + nameContains, +}: { + categoryId?: string; + nameContains?: string; +}) { + const url = `/api/template-center/template`; + + const response = await axiosInstance?.get<{ + code: number; + data?: { + templates: TemplateSummary[]; + }; + message: string; + }>(url, { + params: { + category_id: categoryId, + name_contains: nameContains, + }, + }); + + const data = response?.data; + + if (data?.code === 0 && data.data) { + return data.data.templates; + } + + return Promise.reject(data); +} + +export async function getTemplateById (viewId: string) { + const url = `/api/template-center/template/${viewId}`; + const response = await axiosInstance?.get<{ + code: number; + data?: Template; + message: string; + }>(url); + + const data = response?.data; + + if (data?.code === 0 && data.data) { + return data.data; + } + + return Promise.reject(data); +} + +export async function deleteTemplate (viewId: string) { + const url = `/api/template-center/template/${viewId}`; + const response = await axiosInstance?.delete<{ + code: number; + message: string; + }>(url); + + if (response?.data.code === 0) { + return; + } + + return Promise.reject(response?.data.message); +} + +export async function getTemplateCategories () { + const url = '/api/template-center/category'; + const response = await axiosInstance?.get<{ + code: number; + data?: { + categories: TemplateCategory[] + + }; + message: string; + }>(url); + + const data = response?.data; + + if (data?.code === 0 && data.data) { + return data.data.categories; + } + + return Promise.reject(data); +} + +export async function addTemplateCategory (category: TemplateCategoryFormValues) { + const url = '/api/template-center/category'; + const response = await axiosInstance?.post<{ + code: number; + message: string; + }>(url, category); + + if (response?.data.code === 0) { + return; + } + + return Promise.reject(response?.data.message); +} + +export async function updateTemplateCategory (id: string, category: TemplateCategoryFormValues) { + const url = `/api/template-center/category/${id}`; + const response = await axiosInstance?.put<{ + code: number; + message: string; + }>(url, category); + + if (response?.data.code === 0) { + return; + } + + return Promise.reject(response?.data.message); +} + +export async function deleteTemplateCategory (categoryId: string) { + const url = `/api/template-center/category/${categoryId}`; + const response = await axiosInstance?.delete<{ + code: number; + message: string; + }>(url); + + if (response?.data.code === 0) { + return; + } + + return Promise.reject(response?.data.message); +} + +export async function getTemplateCreators () { + const url = '/api/template-center/creator'; + const response = await axiosInstance?.get<{ + code: number; + data?: { + creators: TemplateCreator[]; + }; + message: string; + }>(url); + + const data = response?.data; + + if (data?.code === 0 && data.data) { + return data.data.creators; + } + + return Promise.reject(data); +} + +export async function createTemplateCreator (creator: TemplateCreatorFormValues) { + const url = '/api/template-center/creator'; + const response = await axiosInstance?.post<{ + code: number; + message: string; + }>(url, creator); + + if (response?.data.code === 0) { + return; + } + + return Promise.reject(response?.data.message); +} + +export async function updateTemplateCreator (creatorId: string, creator: TemplateCreatorFormValues) { + const url = `/api/template-center/creator/${creatorId}`; + const response = await axiosInstance?.put<{ + code: number; + message: string; + }>(url, creator); + + if (response?.data.code === 0) { + return; + } + + return Promise.reject(response?.data.message); +} + +export async function deleteTemplateCreator (creatorId: string) { + const url = `/api/template-center/creator/${creatorId}`; + const response = await axiosInstance?.delete<{ + code: number; + message: string; + }>(url); + + if (response?.data.code === 0) { + return; + } + + return Promise.reject(response?.data.message); +} + +export async function uploadFileToCDN (file: File) { + const url = '/api/template-center/avatar'; + const formData = new FormData(); + + console.log(file); + formData.append('avatar', file); + + const response = await axiosInstance?.request<{ + code: number; + data?: { + file_id: string; + }; + message: string; + }>({ + method: 'PUT', + url, + data: formData, + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + const data = response?.data; + + if (data?.code === 0 && data.data) { + return axiosInstance?.defaults.baseURL + '/api/template-center/avatar/' + data.data.file_id; + } + + return Promise.reject(data); +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/application/services/js-services/http/utils.ts b/frontend/appflowy_web_app/src/application/services/js-services/http/utils.ts index 172f233d2f..aa197a7516 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/http/utils.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/http/utils.ts @@ -1,4 +1,4 @@ -export function blobToBytes(blob: Blob): Promise { +export function blobToBytes (blob: Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); diff --git a/frontend/appflowy_web_app/src/application/services/js-services/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/index.ts index 04f9778edf..91fb1bf4e3 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/index.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/index.ts @@ -13,6 +13,11 @@ import { APIService } from '@/application/services/js-services/http'; import { AFService, AFServiceConfig } from '@/application/services/services.type'; import { emit, EventType } from '@/application/session'; import { afterAuth, AUTH_CALLBACK_URL, withSignIn } from '@/application/session/sign_in'; +import { + TemplateCategoryFormValues, + TemplateCreatorFormValues, + UploadTemplatePayload, +} from '@/application/template.type'; import { nanoid } from 'nanoid'; import * as Y from 'yjs'; import { DuplicatePublishView } from '@/application/types'; @@ -36,15 +41,15 @@ export class AFClientService implements AFService { private cacheDatabaseRowFolder: Map> = new Map(); - constructor(config: AFServiceConfig) { + constructor (config: AFServiceConfig) { APIService.initAPIService(config.cloudConfig); } - getClientId() { + getClientId () { return this.clientId; } - async getPublishViewMeta(namespace: string, publishName: string) { + async getPublishViewMeta (namespace: string, publishName: string) { const name = `${namespace}_${publishName}`; const isLoaded = this.publishViewLoaded.has(name); @@ -56,7 +61,7 @@ export class AFClientService implements AFService { namespace, publishName, }, - isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK + isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK, ); if (!viewMeta) { @@ -66,7 +71,7 @@ export class AFClientService implements AFService { return viewMeta; } - async getPublishView(namespace: string, publishName: string) { + async getPublishView (namespace: string, publishName: string) { const name = `${namespace}_${publishName}`; const isLoaded = this.publishViewLoaded.has(name); @@ -91,7 +96,7 @@ export class AFClientService implements AFService { namespace, publishName, }, - isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK + isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK, ); if (!isLoaded) { @@ -103,7 +108,7 @@ export class AFClientService implements AFService { return doc; } - async getPublishDatabaseViewRows(namespace: string, publishName: string) { + async getPublishDatabaseViewRows (namespace: string, publishName: string) { const name = `${namespace}_${publishName}`; if (!this.publishViewLoaded.has(name) || !this.cacheDatabaseRowDocMap.has(name)) { @@ -133,7 +138,7 @@ export class AFClientService implements AFService { }; } - async getPublishInfo(viewId: string) { + async getPublishInfo (viewId: string) { if (this.publishViewInfo.has(viewId)) { return this.publishViewInfo.get(viewId) as { namespace: string; @@ -159,7 +164,7 @@ export class AFClientService implements AFService { return data; } - async loginAuth(url: string) { + async loginAuth (url: string) { try { console.log('loginAuth', url); await APIService.signInWithUrl(url); @@ -173,45 +178,45 @@ export class AFClientService implements AFService { } @withSignIn() - async signInMagicLink({ email }: { email: string; redirectTo: string }) { + async signInMagicLink ({ email }: { email: string; redirectTo: string }) { return await APIService.signInWithMagicLink(email, AUTH_CALLBACK_URL); } @withSignIn() - async signInGoogle(_: { redirectTo: string }) { + async signInGoogle (_: { redirectTo: string }) { return APIService.signInGoogle(AUTH_CALLBACK_URL); } @withSignIn() - async signInGithub(_: { redirectTo: string }) { + async signInGithub (_: { redirectTo: string }) { return APIService.signInGithub(AUTH_CALLBACK_URL); } @withSignIn() - async signInDiscord(_: { redirectTo: string }) { + async signInDiscord (_: { redirectTo: string }) { return APIService.signInDiscord(AUTH_CALLBACK_URL); } - async getWorkspaces() { + async getWorkspaces () { const data = APIService.getWorkspaces(); return data; } - async getWorkspaceFolder(workspaceId: string) { + async getWorkspaceFolder (workspaceId: string) { const data = await APIService.getWorkspaceFolder(workspaceId); return data; } - async getCurrentUser() { + async getCurrentUser () { const data = await APIService.getCurrentUser(); await APIService.getWorkspaces(); return data; } - async duplicatePublishView(params: DuplicatePublishView) { + async duplicatePublishView (params: DuplicatePublishView) { return APIService.duplicatePublishView(params.workspaceId, { dest_view_id: params.spaceViewId, published_view_id: params.viewId, @@ -219,27 +224,87 @@ export class AFClientService implements AFService { }); } - createCommentOnPublishView(viewId: string, content: string, replyCommentId: string | undefined): Promise { + createCommentOnPublishView (viewId: string, content: string, replyCommentId: string | undefined): Promise { return APIService.createGlobalCommentOnPublishView(viewId, content, replyCommentId); } - deleteCommentOnPublishView(viewId: string, commentId: string): Promise { + deleteCommentOnPublishView (viewId: string, commentId: string): Promise { return APIService.deleteGlobalCommentOnPublishView(viewId, commentId); } - getPublishViewGlobalComments(viewId: string): Promise { + getPublishViewGlobalComments (viewId: string): Promise { return APIService.getPublishViewComments(viewId); } - getPublishViewReactions(viewId: string, commentId?: string): Promise> { + getPublishViewReactions (viewId: string, commentId?: string): Promise> { return APIService.getReactions(viewId, commentId); } - addPublishViewReaction(viewId: string, commentId: string, reactionType: string): Promise { + addPublishViewReaction (viewId: string, commentId: string, reactionType: string): Promise { return APIService.addReaction(viewId, commentId, reactionType); } - removePublishViewReaction(viewId: string, commentId: string, reactionType: string): Promise { + removePublishViewReaction (viewId: string, commentId: string, reactionType: string): Promise { return APIService.removeReaction(viewId, commentId, reactionType); } + + async getTemplateCategories () { + return APIService.getTemplateCategories(); + } + + async getTemplateCreators () { + return APIService.getTemplateCreators(); + } + + async createTemplate (template: UploadTemplatePayload) { + return APIService.createTemplate(template); + } + + async updateTemplate (id: string, template: UploadTemplatePayload) { + return APIService.updateTemplate(id, template); + } + + async getTemplateById (id: string) { + return APIService.getTemplateById(id); + } + + async getTemplates (params: { + categoryId?: string; + nameContains?: string; + }) { + return APIService.getTemplates(params); + } + + async deleteTemplate (id: string) { + return APIService.deleteTemplate(id); + } + + async addTemplateCategory (category: TemplateCategoryFormValues) { + return APIService.addTemplateCategory(category); + } + + async updateTemplateCategory (categoryId: string, category: TemplateCategoryFormValues) { + return APIService.updateTemplateCategory(categoryId, category); + } + + async deleteTemplateCategory (categoryId: string) { + return APIService.deleteTemplateCategory(categoryId); + } + + async updateTemplateCreator (creatorId: string, creator: TemplateCreatorFormValues) { + return APIService.updateTemplateCreator(creatorId, creator); + } + + async createTemplateCreator (creator: TemplateCreatorFormValues) { + return APIService.createTemplateCreator(creator); + } + + async deleteTemplateCreator (creatorId: string) { + return APIService.deleteTemplateCreator(creatorId); + } + + async uploadFileToCDN (file: File) { + return APIService.uploadFileToCDN(file); + } + } diff --git a/frontend/appflowy_web_app/src/application/services/services.type.ts b/frontend/appflowy_web_app/src/application/services/services.type.ts index 77b7086fa3..a346800058 100644 --- a/frontend/appflowy_web_app/src/application/services/services.type.ts +++ b/frontend/appflowy_web_app/src/application/services/services.type.ts @@ -1,6 +1,13 @@ import { YDoc } from '@/application/collab.type'; import { GlobalComment, Reaction } from '@/application/comment.type'; import { ViewMeta } from '@/application/db/tables/view_metas'; +import { + Template, + TemplateCategory, + TemplateCategoryFormValues, + TemplateCreator, TemplateCreatorFormValues, TemplateSummary, + UploadTemplatePayload, +} from '@/application/template.type'; import * as Y from 'yjs'; import { DuplicatePublishView, FolderView, User, Workspace } from '@/application/types'; @@ -24,11 +31,12 @@ export interface PublishService { getPublishDatabaseViewRows: ( namespace: string, publishName: string, - rowIds?: string[] + rowIds?: string[], ) => Promise<{ rows: Y.Map; destroy: () => void; }>; + getPublishViewGlobalComments: (viewId: string) => Promise; createCommentOnPublishView: (viewId: string, content: string, replyCommentId?: string) => Promise; deleteCommentOnPublishView: (viewId: string, commentId: string) => Promise; @@ -46,4 +54,22 @@ export interface PublishService { getWorkspaceFolder: (workspaceId: string) => Promise; getCurrentUser: () => Promise; duplicatePublishView: (params: DuplicatePublishView) => Promise; + + getTemplateCategories: () => Promise; + addTemplateCategory: (category: TemplateCategoryFormValues) => Promise; + deleteTemplateCategory: (categoryId: string) => Promise; + getTemplateCreators: () => Promise; + createTemplateCreator: (creator: TemplateCreatorFormValues) => Promise; + deleteTemplateCreator: (creatorId: string) => Promise; + getTemplateById: (id: string) => Promise