diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 10aa1f9df1..93a8eb77e1 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -170,7 +170,7 @@ SPEC CHECKSUMS: file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c + fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 @@ -191,4 +191,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca -COCOAPODS: 1.11.3 +COCOAPODS: 1.15.2 diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart index 337ce2549d..d4f0766626 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart @@ -25,14 +25,14 @@ class AboutSettingGroup extends StatelessWidget { trailing: const Icon( Icons.chevron_right, ), - onTap: () => afLaunchUrlString('https://appflowy.io/privacy/app'), + onTap: () => afLaunchUrlString('https://appflowy.io/privacy'), ), MobileSettingItem( name: LocaleKeys.settings_mobile_termsAndConditions.tr(), trailing: const Icon( Icons.chevron_right, ), - onTap: () => afLaunchUrlString('https://appflowy.io/terms/app'), + onTap: () => afLaunchUrlString('https://appflowy.io/terms'), ), if (kDebugMode) MobileSettingItem( diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart index 7b0cf7c5fe..89b782d231 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart @@ -54,16 +54,8 @@ class DesktopSignInScreen extends StatelessWidget { const SignInAnonymousButtonV2(), const VSpace(10), - SwitchSignInSignUpButton( - onTap: () { - final type = state.loginType == LoginType.signIn - ? LoginType.signUp - : LoginType.signIn; - context - .read() - .add(SignInEvent.switchLoginType(type)); - }, - ), + // sign in agreement + const SignInAgreement(), // loading status const VSpace(indicatorMinHeight), diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart index 2e2e1e8f39..ce0e959aca 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart @@ -39,16 +39,7 @@ class MobileSignInScreen extends StatelessWidget { const VSpace(spacing), const SignInAnonymousButtonV2(), const VSpace(spacing), - SwitchSignInSignUpButton( - onTap: () { - final type = state.loginType == LoginType.signIn - ? LoginType.signUp - : LoginType.signIn; - context.read().add( - SignInEvent.switchLoginType(type), - ); - }, - ), + const SignInAgreement(), const VSpace(spacing), _buildSettingsButton(context), if (!isAuthEnabled) const Spacer(flex: 2), diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart index b6d5639ee0..e32a9e2908 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart @@ -1,14 +1,14 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:string_validator/string_validator.dart'; +import 'package:toastification/toastification.dart'; class SignInWithMagicLinkButtons extends StatefulWidget { const SignInWithMagicLinkButtons({super.key}); @@ -53,18 +53,19 @@ class _SignInWithMagicLinkButtonsState void _sendMagicLink(BuildContext context, String email) { if (!isEmail(email)) { - return showSnackBarMessage( + return showToastNotification( context, - LocaleKeys.signIn_invalidEmail.tr(), - duration: const Duration(seconds: 8), + message: LocaleKeys.signIn_invalidEmail.tr(), + type: ToastificationType.error, ); } context.read().add(SignInEvent.signedWithMagicLink(email)); - showSnackBarMessage( - context, - LocaleKeys.signIn_magicLinkSent.tr(), - duration: const Duration(seconds: 1000), + + showConfirmDialog( + context: context, + title: LocaleKeys.signIn_magicLinkSent.tr(), + description: LocaleKeys.signIn_magicLinkSentDescription.tr(), ); } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart new file mode 100644 index 0000000000..7976e63667 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart @@ -0,0 +1,44 @@ +import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +class SignInAgreement extends StatelessWidget { + const SignInAgreement({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: '${LocaleKeys.web_signInAgreement.tr()} ', + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + TextSpan( + text: '${LocaleKeys.web_termOfUse.tr()} ', + style: const TextStyle(color: Colors.blue, fontSize: 12), + mouseCursor: SystemMouseCursors.click, + recognizer: TapGestureRecognizer() + ..onTap = () => afLaunchUrlString('https://appflowy.io/terms'), + ), + TextSpan( + text: '${LocaleKeys.web_and.tr()} ', + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + TextSpan( + text: LocaleKeys.web_privacyPolicy.tr(), + style: const TextStyle(color: Colors.blue, fontSize: 12), + mouseCursor: SystemMouseCursors.click, + recognizer: TapGestureRecognizer() + ..onTap = () => afLaunchUrlString('https://appflowy.io/privacy'), + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart index 974e2b5927..c1c2bfecf9 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart @@ -1,5 +1,7 @@ export 'magic_link_sign_in_buttons.dart'; export 'sign_in_anonymous_button.dart'; export 'sign_in_or_logout_button.dart'; -export 'switch_sign_in_sign_up_button.dart'; + +// export 'switch_sign_in_sign_up_button.dart'; export 'third_party_sign_in_buttons.dart'; +export 'sign_in_agreement.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart index d82e81f48a..4dd950c766 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart @@ -223,9 +223,55 @@ class SpaceCancelOrConfirmButton extends StatelessWidget { } } -class ConfirmDeletionPopup extends StatefulWidget { - const ConfirmDeletionPopup({ +class SpaceOkButton extends StatelessWidget { + const SpaceOkButton({ super.key, + required this.onConfirm, + required this.confirmButtonName, + this.confirmButtonColor, + }); + + final VoidCallback onConfirm; + final String confirmButtonName; + final Color? confirmButtonColor; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + DecoratedBox( + decoration: ShapeDecoration( + color: confirmButtonColor ?? Theme.of(context).colorScheme.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: FlowyButton( + useIntrinsicWidth: true, + margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0), + radius: BorderRadius.circular(8), + text: FlowyText.regular( + confirmButtonName, + color: Colors.white, + ), + onTap: onConfirm, + ), + ), + ], + ); + } +} + +enum ConfirmPopupStyle { + onlyOk, + cancelAndOk, +} + +class ConfirmPopup extends StatefulWidget { + const ConfirmPopup({ + super.key, + this.style = ConfirmPopupStyle.cancelAndOk, required this.title, required this.description, required this.onConfirm, @@ -234,12 +280,13 @@ class ConfirmDeletionPopup extends StatefulWidget { final String title; final String description; final VoidCallback onConfirm; + final ConfirmPopupStyle style; @override - State createState() => _ConfirmDeletionPopupState(); + State createState() => _ConfirmPopupState(); } -class _ConfirmDeletionPopupState extends State { +class _ConfirmPopupState extends State { final focusNode = FocusNode(); @override @@ -262,46 +309,70 @@ class _ConfirmDeletionPopupState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Flexible( - child: FlowyText( - widget.title, - fontSize: 14.0, - overflow: TextOverflow.ellipsis, - ), - ), - const HSpace(6.0), - FlowyButton( - useIntrinsicWidth: true, - text: const FlowySvg(FlowySvgs.upgrade_close_s), - onTap: () => Navigator.of(context).pop(), - ), - ], - ), - const VSpace(8.0), - FlowyText.regular( - widget.description, - fontSize: 12.0, - color: Theme.of(context).hintColor, - maxLines: 3, - lineHeight: 1.4, - ), + _buildTitle(), + const VSpace(6.0), + _buildDescription(), const VSpace(20.0), - SpaceCancelOrConfirmButton( - onCancel: () => Navigator.of(context).pop(), - onConfirm: () { - widget.onConfirm(); - Navigator.of(context).pop(); - }, - confirmButtonName: LocaleKeys.space_delete.tr(), - confirmButtonColor: Theme.of(context).colorScheme.error, - ), + _buildStyledButton(context), ], ), ), ); } + + Widget _buildTitle() { + return Row( + children: [ + Expanded( + child: FlowyText( + widget.title, + fontSize: 14.0, + overflow: TextOverflow.ellipsis, + ), + ), + const HSpace(6.0), + FlowyButton( + useIntrinsicWidth: true, + text: const FlowySvg(FlowySvgs.upgrade_close_s), + onTap: () => Navigator.of(context).pop(), + ), + ], + ); + } + + Widget _buildDescription() { + return FlowyText.regular( + widget.description, + fontSize: 12.0, + color: Theme.of(context).hintColor, + maxLines: 3, + lineHeight: 1.4, + ); + } + + Widget _buildStyledButton(BuildContext context) { + switch (widget.style) { + case ConfirmPopupStyle.onlyOk: + return SpaceOkButton( + onConfirm: () { + widget.onConfirm(); + Navigator.of(context).pop(); + }, + confirmButtonName: LocaleKeys.button_ok.tr(), + confirmButtonColor: Theme.of(context).colorScheme.primary, + ); + case ConfirmPopupStyle.cancelAndOk: + return SpaceCancelOrConfirmButton( + onCancel: () => Navigator.of(context).pop(), + onConfirm: () { + widget.onConfirm(); + Navigator.of(context).pop(); + }, + confirmButtonName: LocaleKeys.space_delete.tr(), + confirmButtonColor: Theme.of(context).colorScheme.error, + ); + } + } } class SpacePopup extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 8d1196b503..1fe4cc7502 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -289,10 +289,11 @@ void showToastNotification( BuildContext context, { required String message, String? description, + ToastificationType type = ToastificationType.success, }) { toastification.show( context: context, - type: ToastificationType.success, + type: type, style: ToastificationStyle.flat, title: FlowyText(message), description: description != null @@ -329,7 +330,7 @@ Future showConfirmDeletionDialog({ ), child: SizedBox( width: 440, - child: ConfirmDeletionPopup( + child: ConfirmPopup( title: title, description: description, onConfirm: onConfirm, @@ -339,3 +340,30 @@ Future showConfirmDeletionDialog({ }, ); } + +Future showConfirmDialog({ + required BuildContext context, + required String title, + required String description, + VoidCallback? onConfirm, +}) { + return showDialog( + context: context, + builder: (_) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + child: SizedBox( + width: 440, + child: ConfirmPopup( + title: title, + description: description, + onConfirm: () => onConfirm?.call(), + style: ConfirmPopupStyle.onlyOk, + ), + ), + ); + }, + ); +} diff --git a/frontend/appflowy_web_app/package.json b/frontend/appflowy_web_app/package.json index 8a9be42420..0f7c566a6f 100644 --- a/frontend/appflowy_web_app/package.json +++ b/frontend/appflowy_web_app/package.json @@ -24,7 +24,7 @@ "coverage": "pnpm run test:unit && pnpm run test:components" }, "dependencies": { - "@appflowyinc/client-api-wasm": "0.1.1", + "@appflowyinc/client-api-wasm": "0.1.2", "@atlaskit/primitives": "^5.5.3", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", diff --git a/frontend/appflowy_web_app/pnpm-lock.yaml b/frontend/appflowy_web_app/pnpm-lock.yaml index c7c9daa795..cbee7177c7 100644 --- a/frontend/appflowy_web_app/pnpm-lock.yaml +++ b/frontend/appflowy_web_app/pnpm-lock.yaml @@ -1,13 +1,9 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - dependencies: '@appflowyinc/client-api-wasm': - specifier: 0.1.1 - version: 0.1.1 + specifier: 0.1.2 + version: 0.1.2 '@atlaskit/primitives': specifier: ^5.5.3 version: 5.7.0(@types/react@18.2.66)(react@18.2.0) @@ -451,8 +447,8 @@ packages: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - /@appflowyinc/client-api-wasm@0.1.1: - resolution: {integrity: sha512-7+/TCmzMi9KrxX3HFLJv9R6ON2AO5xQavV547ii7RZM8+5bZJakuf6+pnyCzOquQX07q3ZYwJCa3MIgDvficaA==} + /@appflowyinc/client-api-wasm@0.1.2: + resolution: {integrity: sha512-+v0hs7/7BVKtgev/Bcbr0u2HLDhUuw4ZvZTaMddI+06HK8vt5S52dMaZKUcMvh1eUjVX8hjC6Mfe0X/yHqvFgA==} dev: false /@atlaskit/analytics-next-stable-react-context@1.0.1(react@18.2.0): @@ -11666,3 +11662,7 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} dev: true + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts index 2832a639f9..c1135ab747 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts @@ -513,6 +513,7 @@ export function observeDeepRow( export function useRowDataSelector(rowId: string) { const rowMap = useRowDocMap(); + const rowSharedRoot = rowMap?.get(rowId)?.getMap(YjsEditorKey.data_section); const row = rowSharedRoot?.get(YjsEditorKey.database_row); diff --git a/frontend/appflowy_web_app/src/application/db/tables/view_metas.ts b/frontend/appflowy_web_app/src/application/db/tables/view_metas.ts index 1f39ac19f8..8cc3573038 100644 --- a/frontend/appflowy_web_app/src/application/db/tables/view_metas.ts +++ b/frontend/appflowy_web_app/src/application/db/tables/view_metas.ts @@ -6,6 +6,8 @@ export type ViewMeta = { child_views: PublishViewInfo[]; ancestor_views: PublishViewInfo[]; + + visible_view_ids: string[]; } & PublishViewInfo; export type ViewMetasTable = { diff --git a/frontend/appflowy_web_app/src/application/services/js-services/cache/__tests__/cache.test.ts b/frontend/appflowy_web_app/src/application/services/js-services/cache/__tests__/cache.test.ts index fae533182f..d49ddcab39 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/cache/__tests__/cache.test.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/cache/__tests__/cache.test.ts @@ -9,6 +9,7 @@ import { } from '@/application/services/js-services/cache'; import { openCollabDB, db } from '@/application/db'; import { StrategyType } from '@/application/services/js-services/cache/types'; +import * as Y from 'yjs'; jest.mock('@/application/ydoc/apply', () => ({ applyYDoc: jest.fn(), @@ -118,9 +119,8 @@ describe('Cache functions', () => { describe('getBatchCollabs', () => { it('should return empty array when no cache found', async () => { - (openCollabDB as jest.Mock).mockResolvedValue(undefined); - const collabs = await getBatchCollabs(['1', '2', '3']); - expect(collabs).toEqual([]); + (openCollabDB as jest.Mock).mockResolvedValue(new Y.Doc()); + await expect(getBatchCollabs(['1', '2', '3'])).rejects.toThrow('No cache found'); }); it('should return collabs when cache found', async () => { diff --git a/frontend/appflowy_web_app/src/application/services/js-services/cache/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/cache/index.ts index a4db114630..51ea75f511 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/cache/index.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/cache/index.ts @@ -110,6 +110,8 @@ export async function getPublishViewMeta< export async function getPublishView< T extends { data: number[]; + rows?: Record; + visibleViewIds?: string[]; meta: { view: PublishViewInfo; child_views: PublishViewInfo[]; @@ -176,12 +178,15 @@ export async function revalidatePublishViewMeta< >(name: string, fetcher: Fetcher) { const { view, child_views, ancestor_views } = await fetcher(); + const dbView = await db.view_metas.get(name); + await db.view_metas.put( { publish_name: name, ...view, child_views: child_views, ancestor_views: ancestor_views, + visible_view_ids: dbView?.visible_view_ids ?? [], }, name ); @@ -193,10 +198,11 @@ export async function revalidatePublishView< T extends { data: number[]; rows?: Record; + visibleViewIds?: string[]; meta: PublishViewMetaData; } >(name: string, fetcher: Fetcher, collab: YDoc) { - const { data, meta, rows } = await fetcher(); + const { data, meta, rows, visibleViewIds = [] } = await fetcher(); await db.view_metas.put( { @@ -204,6 +210,7 @@ export async function revalidatePublishView< ...meta.view, child_views: meta.child_views, ancestor_views: meta.ancestor_views, + visible_view_ids: visibleViewIds, }, name ); @@ -222,7 +229,22 @@ export async function revalidatePublishView< } export async function getBatchCollabs(names: string[]) { - const collabs = await Promise.all(names.map((name) => openCollabDB(name))); + const getRowDoc = async (name: string) => { + const doc = await openCollabDB(name); + const exist = hasCollabCache(doc); + + if (!exist) { + return Promise.reject(new Error('No cache found')); + } + + return doc; + }; + + const collabs = await Promise.all( + names.map((name) => { + return getRowDoc(name); + }) + ); return collabs; } 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 9e88466b83..21d739317c 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 @@ -8,9 +8,18 @@ import { } from '@/application/services/js-services/cache'; import { StrategyType } from '@/application/services/js-services/cache/types'; import { fetchPublishView, fetchPublishViewMeta, fetchViewInfo } from '@/application/services/js-services/fetch'; +import { + initAPIService, + signInGoogle, + signInWithMagicLink, + signInGithub, + signInDiscord, + signInWithUrl, +} from '@/application/services/js-services/wasm/client_api'; 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 { nanoid } from 'nanoid'; -import { initAPIService } from '@/application/services/js-services/wasm/client_api'; import * as Y from 'yjs'; export class AFClientService implements AFService { @@ -38,6 +47,10 @@ export class AFClientService implements AFService { }); } + getClientId() { + return this.clientId; + } + async getPublishViewMeta(namespace: string, publishName: string) { const viewMeta = await getPublishViewMeta( () => { @@ -109,12 +122,13 @@ export class AFClientService implements AFService { } const rowsFolder: Y.Map = rootRowsDoc.getMap(); - const docs = await getBatchCollabs(rowIds); + const docs = await getBatchCollabs(rowIds.map((id) => `${name}_${id}`)); docs.forEach((doc, index) => { rowsFolder.set(rowIds[index], doc); }); + console.log('getPublishDatabaseViewRows', docs); return { rows: rowsFolder, destroy: () => { @@ -149,4 +163,37 @@ export class AFClientService implements AFService { return data; } + + async loginAuth(url: string) { + try { + console.log('loginAuth', url); + await signInWithUrl(url); + emit(EventType.SESSION_VALID); + afterAuth(); + return; + } catch (e) { + emit(EventType.SESSION_INVALID); + return Promise.reject(e); + } + } + + @withSignIn() + async signInMagicLink({ email }: { email: string; redirectTo: string }) { + return await signInWithMagicLink(email, AUTH_CALLBACK_URL); + } + + @withSignIn() + async signInGoogle(_: { redirectTo: string }) { + return await signInGoogle(AUTH_CALLBACK_URL); + } + + @withSignIn() + async signInGithub(_: { redirectTo: string }) { + return await signInGithub(AUTH_CALLBACK_URL); + } + + @withSignIn() + async signInDiscord(_: { redirectTo: string }) { + return await signInDiscord(AUTH_CALLBACK_URL); + } } diff --git a/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts b/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts index 9ee174b6c6..b3865b7671 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts @@ -1,6 +1,7 @@ +import { getToken, invalidToken, isTokenValid, refreshToken } from '@/application/session/token'; import { ClientAPI } from '@appflowyinc/client-api-wasm'; import { AFCloudConfig } from '@/application/services/services.type'; -import { PublishViewMetaData } from '@/application/collab.type'; +import { PublishViewMetaData, ViewLayout } from '@/application/collab.type'; let client: ClientAPI; @@ -14,13 +15,9 @@ export function initAPIService( return; } - window.refresh_token = () => { - // - }; + window.refresh_token = refreshToken; - window.invalid_token = () => { - // invalidToken(); - }; + window.invalid_token = invalidToken; client = ClientAPI.new({ base_url: config.baseURL, @@ -34,16 +31,45 @@ export function initAPIService( }, }); + if (isTokenValid()) { + client.restore_token(getToken() || ''); + } + client.subscribe(); } export async function getPublishView(publishNamespace: string, publishName: string) { const data = await client.get_publish_view(publishNamespace, publishName); - return { - data: data.data, - meta: JSON.parse(data.meta.data) as PublishViewMetaData, - }; + const meta = JSON.parse(data.meta.data) as PublishViewMetaData; + + if (meta.view.layout === ViewLayout.Document) { + return { + data: data.data, + meta, + }; + } + + try { + const decoder = new TextDecoder('utf-8'); + const jsonStr = decoder.decode(new Uint8Array(data.data)); + const res = JSON.parse(jsonStr) as { + database_collab: number[]; + database_row_collabs: Record; + database_row_document_collabs: Record; + visible_database_view_ids: string[]; + }; + + console.log('getPublishView', res); + return { + data: res.database_collab, + rows: res.database_row_collabs, + visibleViewIds: res.visible_database_view_ids, + meta, + }; + } catch (e) { + return Promise.reject(e); + } } export async function getPublishInfoWithViewId(viewId: string) { @@ -56,3 +82,33 @@ export async function getPublishViewMeta(publishNamespace: string, publishName: return metadata; } + +export async function signInWithUrl(url: string) { + return client.sign_in_with_url(url); +} + +export async function signInWithMagicLink(email: string, redirectTo: string) { + return client.sign_in_with_magic_link(email, redirectTo); +} + +export async function signInGoogle(redirectTo: string) { + return signInProvider('google', redirectTo); +} + +export async function signInProvider(provider: string, redirectTo: string) { + try { + const { url } = await client.generate_oauth_url_with_provider(provider, redirectTo); + + window.open(url, '_current'); + } catch (e) { + return Promise.reject(e); + } +} + +export async function signInGithub(redirectTo: string) { + return signInProvider('github', redirectTo); +} + +export async function signInDiscord(redirectTo: string) { + return signInProvider('discord', redirectTo); +} 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 99076175a6..1eea995246 100644 --- a/frontend/appflowy_web_app/src/application/services/services.type.ts +++ b/frontend/appflowy_web_app/src/application/services/services.type.ts @@ -15,6 +15,7 @@ export interface AFCloudConfig { } export interface PublishService { + getClientId: () => string; getPublishViewMeta: (namespace: string, publishName: string) => Promise; getPublishView: (namespace: string, publishName: string) => Promise; getPublishInfo: (viewId: string) => Promise<{ namespace: string; publishName: string }>; @@ -26,4 +27,10 @@ export interface PublishService { rows: Y.Map; destroy: () => void; }>; + + loginAuth: (url: string) => Promise; + signInMagicLink: (params: { email: string; redirectTo: string }) => Promise; + signInGoogle: (params: { redirectTo: string }) => Promise; + signInGithub: (params: { redirectTo: string }) => Promise; + signInDiscord: (params: { redirectTo: string }) => Promise; } diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts index 8e81f4ed5f..36f178cc57 100644 --- a/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts @@ -21,4 +21,28 @@ export class AFClientService implements AFService { async getPublishDatabaseViewRows(_namespace: string, _publishName: string, _rowIds: string[]) { return Promise.reject('Method not implemented'); } + + getClientId(): string { + return ''; + } + + loginAuth(_: string): Promise { + return Promise.resolve(undefined); + } + + signInDiscord(_params: { redirectTo: string }): Promise { + return Promise.resolve(undefined); + } + + signInGithub(_params: { redirectTo: string }): Promise { + return Promise.resolve(undefined); + } + + signInGoogle(_params: { redirectTo: string }): Promise { + return Promise.resolve(undefined); + } + + signInMagicLink(_params: { email: string; redirectTo: string }): Promise { + return Promise.resolve(undefined); + } } diff --git a/frontend/appflowy_web_app/src/application/session/event.ts b/frontend/appflowy_web_app/src/application/session/event.ts new file mode 100644 index 0000000000..4b562baa78 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/session/event.ts @@ -0,0 +1,24 @@ +import { EventEmitter } from 'events'; + +const event = new EventEmitter(); + +export enum EventType { + SESSION_EXPIRED = 'session_expired', + SESSION_REFRESH = 'session_refresh', + SESSION_INVALID = 'session_invalid', + SESSION_VALID = 'session_valid', +} + +export type Listener = (data: T) => void; + +export function on(eventType: EventType, listener: Listener) { + event.on(eventType, listener); + + return () => { + event.off(eventType, listener); + }; +} + +export function emit(eventType: EventType, data?: T) { + event.emit(eventType, data); +} diff --git a/frontend/appflowy_web_app/src/application/session/index.ts b/frontend/appflowy_web_app/src/application/session/index.ts new file mode 100644 index 0000000000..0ef93842ad --- /dev/null +++ b/frontend/appflowy_web_app/src/application/session/index.ts @@ -0,0 +1 @@ +export * from './event'; diff --git a/frontend/appflowy_web_app/src/application/session/sign_in.ts b/frontend/appflowy_web_app/src/application/session/sign_in.ts new file mode 100644 index 0000000000..a78c3229a6 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/session/sign_in.ts @@ -0,0 +1,51 @@ +export function saveRedirectTo(redirectTo: string) { + localStorage.setItem('redirectTo', redirectTo); +} + +export function getRedirectTo() { + return localStorage.getItem('redirectTo'); +} + +export function clearRedirectTo() { + localStorage.removeItem('redirectTo'); +} + +export const AUTH_CALLBACK_PATH = '/auth/callback'; +export const AUTH_CALLBACK_URL = `${window.location.origin}${AUTH_CALLBACK_PATH}`; + +export function withSignIn() { + return function ( + // eslint-disable-next-line + _target: any, + _propertyKey: string, + descriptor: PropertyDescriptor + ) { + const originalMethod = descriptor.value; + + // eslint-disable-next-line + descriptor.value = async function (args: { redirectTo: string }) { + const redirectTo = args.redirectTo; + + saveRedirectTo(redirectTo); + + console.log('=====saveRedirectTo', redirectTo); + try { + await originalMethod.apply(this, [args]); + } catch (e) { + console.error(e); + return Promise.reject(e); + } + }; + + return descriptor; + }; +} + +export function afterAuth() { + const redirectTo = getRedirectTo(); + + if (redirectTo) { + clearRedirectTo(); + window.location.href = redirectTo; + } +} diff --git a/frontend/appflowy_web_app/src/application/session/token.ts b/frontend/appflowy_web_app/src/application/session/token.ts new file mode 100644 index 0000000000..174e350490 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/session/token.ts @@ -0,0 +1,20 @@ +import { emit, EventType } from '@/application/session/event'; + +export function refreshToken(token: string) { + localStorage.removeItem('token'); + localStorage.setItem('token', token); + emit(EventType.SESSION_REFRESH, token); +} + +export function invalidToken() { + localStorage.removeItem('token'); + emit(EventType.SESSION_INVALID); +} + +export function isTokenValid() { + return !!localStorage.getItem('token'); +} + +export function getToken() { + return localStorage.getItem('token'); +} diff --git a/frontend/appflowy_web_app/src/assets/login.svg b/frontend/appflowy_web_app/src/assets/login.svg new file mode 100644 index 0000000000..348b76aa69 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/login.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/login/discord.svg b/frontend/appflowy_web_app/src/assets/login/discord.svg new file mode 100644 index 0000000000..901b2fd9e0 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/login/discord.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/login/github.svg b/frontend/appflowy_web_app/src/assets/login/github.svg new file mode 100644 index 0000000000..0a73a316ab --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/login/github.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/login/google.svg b/frontend/appflowy_web_app/src/assets/login/google.svg new file mode 100644 index 0000000000..84bee691e0 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/login/google.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/app/App.tsx b/frontend/appflowy_web_app/src/components/app/App.tsx index c66556d8f7..e029a9f207 100644 --- a/frontend/appflowy_web_app/src/components/app/App.tsx +++ b/frontend/appflowy_web_app/src/components/app/App.tsx @@ -1,4 +1,7 @@ +import { AUTH_CALLBACK_PATH } from '@/application/session/sign_in'; import NotFound from '@/components/error/NotFound'; +import LoginAuth from '@/components/login/LoginAuth'; +import LoginPage from '@/pages/LoginPage'; import PublishPage from '@/pages/PublishPage'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import withAppWrapper from '@/components/app/withAppWrapper'; @@ -8,6 +11,8 @@ const AppMain = withAppWrapper(() => { return ( } /> + } /> + } /> } /> } /> diff --git a/frontend/appflowy_web_app/src/components/app/AppConfig.tsx b/frontend/appflowy_web_app/src/components/app/AppConfig.tsx index 7c215bd530..8d5c6c2b64 100644 --- a/frontend/appflowy_web_app/src/components/app/AppConfig.tsx +++ b/frontend/appflowy_web_app/src/components/app/AppConfig.tsx @@ -1,3 +1,5 @@ +import { EventType, on } from '@/application/session'; +import { isTokenValid } from '@/application/session/token'; import { useAppLanguage } from '@/components/app/useAppLanguage'; import { useSnackbar } from 'notistack'; import React, { createContext, useEffect, useState } from 'react'; @@ -19,6 +21,7 @@ const defaultConfig: AFServiceConfig = { export const AFConfigContext = createContext< | { service: AFService | undefined; + isAuthenticated: boolean; } | undefined >(undefined); @@ -26,7 +29,29 @@ export const AFConfigContext = createContext< function AppConfig({ children }: { children: React.ReactNode }) { const [appConfig] = useState(defaultConfig); const [service, setService] = useState(); + const [isAuthenticated, setIsAuthenticated] = React.useState(isTokenValid()); + useEffect(() => { + return on(EventType.SESSION_VALID, () => { + setIsAuthenticated(true); + }); + }, []); + + useEffect(() => { + const handleStorageChange = (event: StorageEvent) => { + if (event.key === 'token') setIsAuthenticated(isTokenValid()); + }; + + window.addEventListener('storage', handleStorageChange); + return () => { + window.removeEventListener('storage', handleStorageChange); + }; + }, []); + useEffect(() => { + return on(EventType.SESSION_INVALID, () => { + setIsAuthenticated(false); + }); + }, []); useAppLanguage(); useEffect(() => { @@ -67,6 +92,7 @@ function AppConfig({ children }: { children: React.ReactNode }) { {children} diff --git a/frontend/appflowy_web_app/src/components/database/Database.tsx b/frontend/appflowy_web_app/src/components/database/Database.tsx index 4b3964e240..5a4544e568 100644 --- a/frontend/appflowy_web_app/src/components/database/Database.tsx +++ b/frontend/appflowy_web_app/src/components/database/Database.tsx @@ -1,9 +1,10 @@ import { YDatabase, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; import { ViewMeta } from '@/application/db/tables/view_metas'; import ComponentLoading from '@/components/_shared/progress/ComponentLoading'; +import DatabaseHeader from '@/components/database/components/header/DatabaseHeader'; import DatabaseRow from '@/components/database/DatabaseRow'; import DatabaseViews from '@/components/database/DatabaseViews'; -import { ViewMetaPreview, ViewMetaProps } from '@/components/view-meta/ViewMetaPreview'; +import { ViewMetaProps } from '@/components/view-meta/ViewMetaPreview'; import React, { Suspense, useCallback, useEffect, useMemo, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import * as Y from 'yjs'; @@ -27,7 +28,7 @@ function Database({ doc, getViewRowsMap, navigateToView, loadViewMeta, loadView, const database = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database) as YDatabase; const rows = database.get(YjsDatabaseKey.views).get(viewId).get(YjsDatabaseKey.row_orders); - return rows.toArray().map((row) => row.get(YjsDatabaseKey.id)); + return rows.toJSON().map((row) => row.id); }, [doc, viewId]); const iidIndex = useMemo(() => { @@ -70,7 +71,12 @@ function Database({ doc, getViewRowsMap, navigateToView, loadViewMeta, loadView, } return ( -
+
}> ) : (
- {viewMeta && } + {viewMeta && }
- +
)} diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx index 66730ed897..f9682e6b5f 100644 --- a/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx +++ b/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx @@ -15,10 +15,12 @@ function DatabaseViews({ onChangeView, viewId, iidIndex, + viewName, }: { onChangeView: (viewId: string) => void; viewId: string; iidIndex: string; + viewName?: string; }) { const { childViews, viewIds } = useDatabaseViewsSelector(iidIndex); @@ -60,7 +62,13 @@ function DatabaseViews({ toggleExpanded, }} > - +
diff --git a/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx b/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx index b189bd59dd..df1efbf840 100644 --- a/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx @@ -1,4 +1,4 @@ -import { useFieldsSelector, useNavigateToRow } from '@/application/database-yjs'; +import { useFieldsSelector } from '@/application/database-yjs'; import CardField from '@/components/database/components/field/CardField'; import React, { memo, useEffect, useMemo } from 'react'; @@ -32,12 +32,12 @@ export const Card = memo(({ groupFieldId, rowId, onResize, isDragging }: CardPro }; }, [onResize, isDragging]); - const navigateToRow = useNavigateToRow(); + // const navigateToRow = useNavigateToRow(); return (
{ - navigateToRow?.(rowId); + // navigateToRow?.(rowId); }} ref={ref} style={{ diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/event/Event.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/event/Event.tsx index ee96745b61..f2dc324409 100644 --- a/frontend/appflowy_web_app/src/components/database/components/calendar/event/Event.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/event/Event.tsx @@ -1,4 +1,4 @@ -import { CalendarEvent, useFieldsSelector, useNavigateToRow } from '@/application/database-yjs'; +import { CalendarEvent, useFieldsSelector } from '@/application/database-yjs'; import { RichTooltip } from '@/components/_shared/popover'; import EventPaper from '@/components/database/components/calendar/event/EventPaper'; import CardField from '@/components/database/components/field/CardField'; @@ -11,7 +11,7 @@ export function Event({ event }: EventWrapperProps) { const fields = useFieldsSelector(); const showFields = useMemo(() => fields.filter((field) => field.fieldId !== fieldId), [fields, fieldId]); - const navigateToRow = useNavigateToRow(); + // const navigateToRow = useNavigateToRow(); const [open, setOpen] = React.useState(false); return ( @@ -20,7 +20,7 @@ export function Event({ event }: EventWrapperProps) {
{ if (window.innerWidth < 768) { - navigateToRow?.(rowId); + // navigateToRow?.(rowId); } else { setOpen((prev) => !prev); } diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx index f84619fe8f..48d1bf0ff7 100644 --- a/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx @@ -1,6 +1,6 @@ import { useFieldsSelector, usePrimaryFieldId } from '@/application/database-yjs'; import EventPaperTitle from '@/components/database/components/calendar/event/EventPaperTitle'; -import OpenAction from '@/components/database/components/database-row/OpenAction'; +// import OpenAction from '@/components/database/components/database-row/OpenAction'; import { Property } from '@/components/database/components/property'; import React from 'react'; @@ -12,9 +12,9 @@ function EventPaper({ rowId }: { rowId: string }) { return (
-
- -
+ {/*
*/} + {/* */} + {/*
*/}
{primaryFieldId && } {fields.map((field) => { diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx index 7c2eb6c648..102e9eb0cc 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx @@ -1,7 +1,6 @@ import { useNavigateToRow, useRowMetaSelector } from '@/application/database-yjs'; import { TextCell as CellType, CellProps } from '@/application/database-yjs/cell.type'; import { TextCell } from '@/components/database/components/cell/text'; -import OpenAction from '@/components/database/components/database-row/OpenAction'; import { getPlatform } from '@/utils/platform'; import React, { useEffect, useMemo, useState } from 'react'; @@ -10,7 +9,7 @@ export function PrimaryCell(props: CellProps) { const meta = useRowMetaSelector(rowId); const icon = meta?.icon; - const [hover, setHover] = useState(false); + const [, setHover] = useState(false); useEffect(() => { const table = document.querySelector('.grid-table'); @@ -61,11 +60,11 @@ export function PrimaryCell(props: CellProps) {
- {hover && ( -
- -
- )} + {/*{hover && (*/} + {/*
*/} + {/* */} + {/*
*/} + {/*)}*/}
); } diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx index 41e52c5086..ce09cb500f 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx @@ -18,6 +18,7 @@ export interface GridTableProps { export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: GridTableProps) => { const ref = useRef(null); const { rows } = useRenderRows(); + const forceUpdate = useCallback((index: number) => { ref.current?.resetAfterRowIndex(index, true); }, []); diff --git a/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx new file mode 100644 index 0000000000..5f99969abf --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx @@ -0,0 +1,38 @@ +import { ViewLayout, ViewMetaIcon } from '@/application/collab.type'; +import { ViewIcon } from '@/components/_shared/view-icon'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +function DatabaseHeader({ + icon, + name, + layout, +}: { + icon?: ViewMetaIcon; + name?: string; + viewId?: string; + layout?: ViewLayout; +}) { + const { t } = useTranslation(); + + return ( +
+
+ {icon?.value ? ( +
{icon?.value}
+ ) : ( + + )} +
+
+ {name || {t('menuAppHeader.defaultNewPageName')}} +
+
+ ); +} + +export default DatabaseHeader; diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx index 7b52d96a84..e5dd601c64 100644 --- a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx @@ -14,6 +14,8 @@ export interface DatabaseTabBarProps { viewIds: string[]; selectedViewId?: string; setSelectedViewId?: (viewId: string) => void; + viewName?: string; + iidIndex: string; } const DatabaseIcons: { @@ -25,7 +27,7 @@ const DatabaseIcons: { }; export const DatabaseTabs = forwardRef( - ({ viewIds, selectedViewId, setSelectedViewId }, ref) => { + ({ viewIds, viewName, iidIndex, selectedViewId, setSelectedViewId }, ref) => { const { t } = useTranslation(); const view = useDatabaseView(); const views = useDatabase().get(YjsDatabaseKey.views); @@ -69,7 +71,7 @@ export const DatabaseTabs = forwardRef( if (!view) return null; const layout = Number(view.get(YjsDatabaseKey.layout)) as DatabaseViewLayout; const Icon = DatabaseIcons[layout]; - const name = view.get(YjsDatabaseKey.name); + const name = viewId === iidIndex ? viewName : view.get(YjsDatabaseKey.name); return ( { return (
- - - + + <> + + + - +
{t('publish.noAccessToVisit')} - - -
{t('publish.createWithAppFlowy')}
+
+
+
{t('publish.createWithAppFlowy')}
- {t('publish.fastWithAI')} - {t('publish.tryItNow')} +
{t('publish.fastWithAI')}
+
{t('publish.tryItNow')}
- +
+ ))} +
+ ); +} + +export default LoginProvider; diff --git a/frontend/appflowy_web_app/src/components/login/MagicLink.tsx b/frontend/appflowy_web_app/src/components/login/MagicLink.tsx new file mode 100644 index 0000000000..7ba078d8fc --- /dev/null +++ b/frontend/appflowy_web_app/src/components/login/MagicLink.tsx @@ -0,0 +1,67 @@ +import { notify } from '@/components/_shared/notify'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { Button, CircularProgress, OutlinedInput } from '@mui/material'; +import React, { useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import validator from 'validator'; + +function MagicLink({ redirectTo }: { redirectTo: string }) { + const { t } = useTranslation(); + const [email, setEmail] = React.useState(''); + const [loading, setLoading] = React.useState(false); + const service = useContext(AFConfigContext)?.service; + const handleSubmit = async () => { + const isValidEmail = validator.isEmail(email); + + if (!isValidEmail) { + notify.error(t('signIn.invalidEmail')); + return; + } + + setLoading(true); + + try { + await service?.signInMagicLink({ + email, + redirectTo, + }); + notify.success(t('signIn.magicLinkSent')); + } catch (e) { + notify.error(t('web.signInError')); + } finally { + setLoading(false); + } + }; + + return ( +
+ setEmail(e.target.value)} + /> + +
+ ); +} + +export default MagicLink; diff --git a/frontend/appflowy_web_app/src/components/login/index.ts b/frontend/appflowy_web_app/src/components/login/index.ts new file mode 100644 index 0000000000..a10c3a83ac --- /dev/null +++ b/frontend/appflowy_web_app/src/components/login/index.ts @@ -0,0 +1 @@ +export * from './Login'; diff --git a/frontend/appflowy_web_app/src/components/publish/CollabView.tsx b/frontend/appflowy_web_app/src/components/publish/CollabView.tsx index d5ae4d9d11..0c31919813 100644 --- a/frontend/appflowy_web_app/src/components/publish/CollabView.tsx +++ b/frontend/appflowy_web_app/src/components/publish/CollabView.tsx @@ -4,10 +4,10 @@ import { usePublishContext } from '@/application/publish'; import ComponentLoading from '@/components/_shared/progress/ComponentLoading'; import { useAppThemeMode } from '@/components/app/useAppThemeMode'; import { Database } from '@/components/database'; -import { useViewMeta } from '@/components/publish/useViewMeta'; -import { ViewMetaProps } from 'src/components/view-meta'; -import React, { useMemo } from 'react'; import { Document } from '@/components/document'; +import { useViewMeta } from '@/components/publish/useViewMeta'; +import React, { useMemo } from 'react'; +import { ViewMetaProps } from 'src/components/view-meta'; import Y from 'yjs'; export interface CollabViewProps { @@ -49,7 +49,7 @@ function CollabView({ doc }: CollabViewProps) { } return ( -
+
); diff --git a/frontend/appflowy_web_app/src/components/publish/header/MoreActions.tsx b/frontend/appflowy_web_app/src/components/publish/header/MoreActions.tsx index 4497eb6314..9c283f95ac 100644 --- a/frontend/appflowy_web_app/src/components/publish/header/MoreActions.tsx +++ b/frontend/appflowy_web_app/src/components/publish/header/MoreActions.tsx @@ -1,4 +1,6 @@ +// import { invalidToken } from '@/application/session/token'; import { Popover } from '@/components/_shared/popover'; +// import { AFConfigContext } from '@/components/app/AppConfig'; import { ThemeModeContext } from '@/components/app/useAppThemeMode'; import { openUrl } from '@/utils/url'; import { IconButton } from '@mui/material'; @@ -6,11 +8,14 @@ import React, { useContext, useMemo } from 'react'; import { ReactComponent as MoreIcon } from '@/assets/more.svg'; import { ReactComponent as MoonIcon } from '@/assets/moon.svg'; import { ReactComponent as SunIcon } from '@/assets/sun.svg'; +// import { ReactComponent as LoginIcon } from '@/assets/login.svg'; import { ReactComponent as ReportIcon } from '@/assets/report.svg'; import { useTranslation } from 'react-i18next'; import { ReactComponent as Logo } from '@/assets/logo.svg'; import { ReactComponent as AppflowyLogo } from '@/assets/appflowy.svg'; +// import { useNavigate } from 'react-router-dom'; + function MoreActions() { const { isDark, setDark } = useContext(ThemeModeContext) || {}; const [anchorEl, setAnchorEl] = React.useState(null); @@ -26,8 +31,21 @@ function MoreActions() { const { t } = useTranslation(); + // const navigate = useNavigate(); + + // const isAuthenticated = useContext(AFConfigContext)?.isAuthenticated || false; + // + // const handleLogin = useCallback(() => { + // invalidToken(); + // navigate('/login?redirectTo=' + encodeURIComponent(window.location.href)); + // }, [navigate]); const actions = useMemo(() => { return [ + // { + // Icon: LoginIcon, + // label: isAuthenticated ? t('button.logout') : t('web.login'), + // onClick: handleLogin, + // }, isDark ? { Icon: SunIcon, @@ -51,7 +69,7 @@ function MoreActions() { }, }, ]; - }, [isDark, t, setDark]); + }, [t, isDark, setDark]); return ( <> diff --git a/frontend/appflowy_web_app/src/components/view-meta/ViewMetaPreview.tsx b/frontend/appflowy_web_app/src/components/view-meta/ViewMetaPreview.tsx index 6c555dd641..67310f4036 100644 --- a/frontend/appflowy_web_app/src/components/view-meta/ViewMetaPreview.tsx +++ b/frontend/appflowy_web_app/src/components/view-meta/ViewMetaPreview.tsx @@ -7,7 +7,7 @@ import BuiltInImage6 from '@/assets/cover/m_cover_image_6.png'; import ViewCover, { CoverType } from '@/components/view-meta/ViewCover'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { ViewMetaIcon } from '@/application/collab.type'; +import { ViewLayout, ViewMetaIcon } from '@/application/collab.type'; export interface ViewMetaCover { type: CoverType; @@ -19,6 +19,7 @@ export interface ViewMetaProps { cover?: ViewMetaCover; name?: string; viewId?: string; + layout?: ViewLayout; } export function ViewMetaPreview({ icon, cover, name }: ViewMetaProps) { diff --git a/frontend/appflowy_web_app/src/pages/LoginPage.tsx b/frontend/appflowy_web_app/src/pages/LoginPage.tsx new file mode 100644 index 0000000000..52bfd9d963 --- /dev/null +++ b/frontend/appflowy_web_app/src/pages/LoginPage.tsx @@ -0,0 +1,12 @@ +import { Login } from '@/components/login'; +import React from 'react'; + +function LoginPage() { + return ( +
+ +
+ ); +} + +export default LoginPage; diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 6398ae630a..b2bceb1152 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -34,7 +34,7 @@ "signIn": { "loginTitle": "Login to @:appName", "loginButtonText": "Login", - "loginStartWithAnonymous": "Start with an anonymous session", + "loginStartWithAnonymous": "Continue with an anonymous session", "continueAnonymousUser": "Continue with an anonymous session", "buttonText": "Sign In", "signingInText": "Signing in...", @@ -47,24 +47,25 @@ "unmatchedPasswordError": "Repeat password is not the same as password", "syncPromptMessage": "Syncing the data might take a while. Please don't close this page", "or": "OR", - "signInWithGoogle": "Log in with Google", - "signInWithGithub": "Log in with Github", - "signInWithDiscord": "Log in with Discord", + "signInWithGoogle": "Continue with Google", + "signInWithGithub": "Continue with Github", + "signInWithDiscord": "Continue with Discord", "signUpWithGoogle": "Sign up with Google", "signUpWithGithub": "Sign up with Github", "signUpWithDiscord": "Sign up with Discord", - "signInWith": "Sign in with:", - "signInWithEmail": "Sign in with Email", - "signInWithMagicLink": "Log in with Magic Link", + "signInWith": "Continue with:", + "signInWithEmail": "Continue with Email", + "signInWithMagicLink": "Continue", "signUpWithMagicLink": "Sign up with Magic Link", "pleaseInputYourEmail": "Please enter your email address", "settings": "Settings", - "magicLinkSent": "We emailed a magic link. Click the link to log in.", + "magicLinkSent": "Magic Link sent!", "invalidEmail": "Please enter a valid email address", "alreadyHaveAnAccount": "Already have an account?", "logIn": "Log in", "generalError": "Something went wrong. Please try again later", - "limitRateError": "For security reasons, you can only request a magic link every 60 seconds" + "limitRateError": "For security reasons, you can only request a magic link every 60 seconds", + "magicLinkSentDescription": "A Magic Link was sent to your email. Click the link to complete your login. The link will expire after 5 minutes." }, "workspace": { "chooseWorkspace": "Choose your workspace", @@ -335,9 +336,9 @@ "logout": "Log out", "deleteAccount": "Delete account", "back": "Back", - "signInGoogle": "Sign in with Google", - "signInGithub": "Sign in with Github", - "signInDiscord": "Sign in with Discord", + "signInGoogle": "Continue with Google", + "signInGithub": "Continue with Github", + "signInDiscord": "Continue with Discord", "more": "More", "create": "Create", "close": "Close" @@ -2041,7 +2042,6 @@ "upgrade": "Update", "upgradeYourSpace": "Create multiple Spaces", "quicklySwitch": "Quickly switch to the next space", - "duplicate": "Duplicate Space", "movePageToSpace": "Move page to space", "switchSpace": "Switch space" @@ -2066,5 +2066,18 @@ "createWithAppFlowy": "Create a website with AppFlowy", "fastWithAI": "Fast and easy with AI.", "tryItNow": "Try it now" + }, + "web": { + "continue": "Continue", + "or": "or", + "continueWithGoogle": "Continue with Google", + "continueWithGithub": "Continue with GitHub", + "continueWithDiscord": "Continue with Discord", + "signInAgreement": "By clicking \"Continue\" above, you confirm that\nyou have read, understood, and agreed to\nAppFlowy's", + "and": "and", + "termOfUse": "Terms", + "privacyPolicy": "Privacy Policy", + "signInError": "Sign in error", + "login": "Sign up or log in" } }