mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-12-27 23:24:38 +00:00
feat: lock page (#7353)
* feat: lock page * feat: add pageLockStatus bloc * feat: add lock status and unlock status in title bar * feat: add loading lock status * feat: disable moveTo, delete, rename, updateIcon operations if the page is locked * fix: lock toast issue * feat: support locked database * feat: support locked grid * feat: support locked title * feat: support locked board * feat: support locked calendar
This commit is contained in:
parent
552dba5abe
commit
71ce9affbe
@ -17,6 +17,7 @@ import 'package:appflowy/shared/conditional_listenable_builder.dart';
|
||||
import 'package:appflowy/shared/flowy_error_page.dart';
|
||||
import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
@ -205,6 +206,7 @@ class _DesktopBoardPageState extends State<DesktopBoardPage> {
|
||||
onEditStateChanged: widget.onEditStateChanged,
|
||||
focusScope: _focusScope,
|
||||
boardController: _boardController,
|
||||
view: widget.view,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -239,6 +241,7 @@ class _BoardContent extends StatefulWidget {
|
||||
const _BoardContent({
|
||||
required this.boardController,
|
||||
required this.focusScope,
|
||||
required this.view,
|
||||
this.onEditStateChanged,
|
||||
this.shrinkWrap = false,
|
||||
});
|
||||
@ -247,6 +250,7 @@ class _BoardContent extends StatefulWidget {
|
||||
final BoardFocusScope focusScope;
|
||||
final VoidCallback? onEditStateChanged;
|
||||
final bool shrinkWrap;
|
||||
final ViewPB view;
|
||||
|
||||
@override
|
||||
State<_BoardContent> createState() => _BoardContentState();
|
||||
@ -366,7 +370,7 @@ class _BoardContentState extends State<_BoardContent> {
|
||||
scrollManager: scrollManager,
|
||||
),
|
||||
),
|
||||
cardBuilder: (_, column, columnItem) => MultiBlocProvider(
|
||||
cardBuilder: (context, column, columnItem) => MultiBlocProvider(
|
||||
key: ValueKey("board_card_${column.id}_${columnItem.id}"),
|
||||
providers: [
|
||||
BlocProvider<BoardBloc>.value(
|
||||
@ -375,13 +379,24 @@ class _BoardContentState extends State<_BoardContent> {
|
||||
BlocProvider.value(
|
||||
value: context.read<BoardActionsCubit>(),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) => ViewLockStatusBloc(view: widget.view)
|
||||
..add(ViewLockStatusEvent.initial()),
|
||||
),
|
||||
],
|
||||
child: _BoardCard(
|
||||
afGroupData: column,
|
||||
groupItem: columnItem as GroupItem,
|
||||
boardConfig: config,
|
||||
notifier: widget.focusScope,
|
||||
cellBuilder: cellBuilder,
|
||||
child: BlocBuilder<ViewLockStatusBloc, ViewLockStatusState>(
|
||||
builder: (context, state) {
|
||||
return IgnorePointer(
|
||||
ignoring: state.isLocked,
|
||||
child: _BoardCard(
|
||||
afGroupData: column,
|
||||
groupItem: columnItem as GroupItem,
|
||||
boardConfig: config,
|
||||
notifier: widget.focusScope,
|
||||
cellBuilder: cellBuilder,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -1,9 +1,3 @@
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
@ -12,21 +6,26 @@ import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/calendar/application/unschedule_event_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
|
||||
import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import '../../application/row/row_controller.dart';
|
||||
import '../../widgets/row/row_detail.dart';
|
||||
|
||||
import 'calendar_day.dart';
|
||||
import 'layout/sizes.dart';
|
||||
import 'toolbar/calendar_setting_bar.dart';
|
||||
@ -123,8 +122,18 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return CalendarControllerProvider(
|
||||
controller: _eventController,
|
||||
child: BlocProvider<CalendarBloc>.value(
|
||||
value: _calendarBloc,
|
||||
child: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<CalendarBloc>.value(
|
||||
value: _calendarBloc,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => ViewLockStatusBloc(view: widget.view)
|
||||
..add(
|
||||
ViewLockStatusEvent.initial(),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<CalendarBloc, CalendarState>(
|
||||
@ -235,7 +244,21 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
showBorder: false,
|
||||
headerBuilder: _headerNavigatorBuilder,
|
||||
weekDayBuilder: _headerWeekDayBuilder,
|
||||
cellBuilder: _calendarDayBuilder,
|
||||
cellBuilder: (
|
||||
date,
|
||||
calenderEvents,
|
||||
isToday,
|
||||
isInMonth,
|
||||
position,
|
||||
) =>
|
||||
_calendarDayBuilder(
|
||||
context,
|
||||
date,
|
||||
calenderEvents,
|
||||
isToday,
|
||||
isInMonth,
|
||||
position,
|
||||
),
|
||||
useAvailableVerticalSpace: widget.shrinkWrap,
|
||||
),
|
||||
),
|
||||
@ -344,6 +367,7 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
}
|
||||
|
||||
Widget _calendarDayBuilder(
|
||||
BuildContext context,
|
||||
DateTime date,
|
||||
List<CalendarEventData<CalendarDayEvent>> calenderEvents,
|
||||
isToday,
|
||||
@ -355,17 +379,21 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
// is implemnted in the develop branch(WIP). Will be replaced with that.
|
||||
final events = calenderEvents.map((value) => value.event!).toList()
|
||||
..sort((a, b) => a.event.timestamp.compareTo(b.event.timestamp));
|
||||
final isLocked = context.watch<ViewLockStatusBloc>().state.isLocked;
|
||||
|
||||
return CalendarDayCard(
|
||||
viewId: widget.view.id,
|
||||
isToday: isToday,
|
||||
isInMonth: isInMonth,
|
||||
events: events,
|
||||
date: date,
|
||||
rowCache: _calendarBloc.rowCache,
|
||||
onCreateEvent: (date) =>
|
||||
_calendarBloc.add(CalendarEvent.createEvent(date)),
|
||||
position: position,
|
||||
return IgnorePointer(
|
||||
ignoring: isLocked,
|
||||
child: CalendarDayCard(
|
||||
viewId: widget.view.id,
|
||||
isToday: isToday,
|
||||
isInMonth: isInMonth,
|
||||
events: events,
|
||||
date: date,
|
||||
rowCache: _calendarBloc.rowCache,
|
||||
onCreateEvent: (date) =>
|
||||
_calendarBloc.add(CalendarEvent.createEvent(date)),
|
||||
position: position,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import 'package:appflowy/shared/flowy_error_page.dart';
|
||||
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
@ -138,8 +139,16 @@ class _GridPageState extends State<GridPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<GridBloc>(
|
||||
create: (_) => gridBloc,
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<GridBloc>(
|
||||
create: (_) => gridBloc,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => ViewLockStatusBloc(view: widget.view)
|
||||
..add(ViewLockStatusEvent.initial()),
|
||||
),
|
||||
],
|
||||
child: BlocListener<ActionNavigationBloc, ActionNavigationState>(
|
||||
listener: (context, state) {
|
||||
final action = state.action;
|
||||
@ -286,7 +295,10 @@ class _GridPageContentState extends State<GridPageContent> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_GridHeader(headerScrollController: headerScrollController),
|
||||
_GridHeader(
|
||||
headerScrollController: headerScrollController,
|
||||
editable: !context.read<ViewLockStatusBloc>().state.isLocked,
|
||||
),
|
||||
_GridRows(
|
||||
viewId: widget.view.id,
|
||||
scrollController: _scrollController,
|
||||
@ -298,18 +310,30 @@ class _GridPageContentState extends State<GridPageContent> {
|
||||
}
|
||||
|
||||
class _GridHeader extends StatelessWidget {
|
||||
const _GridHeader({required this.headerScrollController});
|
||||
const _GridHeader({
|
||||
required this.headerScrollController,
|
||||
required this.editable,
|
||||
});
|
||||
|
||||
final ScrollController headerScrollController;
|
||||
final bool editable;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<GridBloc, GridState>(
|
||||
Widget child = BlocBuilder<GridBloc, GridState>(
|
||||
builder: (_, state) => GridHeaderSliverAdaptor(
|
||||
viewId: state.viewId,
|
||||
anchorScrollController: headerScrollController,
|
||||
),
|
||||
);
|
||||
|
||||
if (!editable) {
|
||||
child = IgnorePointer(
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
@ -502,12 +526,21 @@ class _GridRowsState extends State<_GridRows> {
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == itemCount - 1) {
|
||||
return Column(
|
||||
final child = Column(
|
||||
key: const Key('grid_footer'),
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: footer,
|
||||
);
|
||||
|
||||
if (context.read<ViewLockStatusBloc>().state.isLocked) {
|
||||
return IgnorePointer(
|
||||
key: const Key('grid_footer'),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
return _renderRow(
|
||||
@ -542,6 +575,7 @@ class _GridRowsState extends State<_GridRows> {
|
||||
rowId: rowId,
|
||||
viewId: viewId,
|
||||
index: index,
|
||||
editable: !context.watch<ViewLockStatusBloc>().state.isLocked,
|
||||
rowController: RowController(
|
||||
viewId: viewId,
|
||||
rowMeta: rowMeta,
|
||||
|
||||
@ -1,27 +1,25 @@
|
||||
import 'package:appflowy/plugins/database/domain/sort_service.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import "package:appflowy/generated/locale_keys.g.dart";
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database/domain/sort_service.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/row/row_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../../widgets/row/accessory/cell_accessory.dart';
|
||||
import '../../../../widgets/row/cells/cell_container.dart';
|
||||
import '../../layout/sizes.dart';
|
||||
|
||||
import 'action.dart';
|
||||
|
||||
class GridRow extends StatelessWidget {
|
||||
@ -35,6 +33,7 @@ class GridRow extends StatelessWidget {
|
||||
required this.openDetailPage,
|
||||
required this.index,
|
||||
this.shrinkWrap = false,
|
||||
required this.editable,
|
||||
});
|
||||
|
||||
final FieldController fieldController;
|
||||
@ -45,6 +44,7 @@ class GridRow extends StatelessWidget {
|
||||
final void Function(BuildContext context) openDetailPage;
|
||||
final int index;
|
||||
final bool shrinkWrap;
|
||||
final bool editable;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -58,7 +58,7 @@ class GridRow extends StatelessWidget {
|
||||
rowContent = Expanded(child: rowContent);
|
||||
}
|
||||
|
||||
return BlocProvider(
|
||||
rowContent = BlocProvider(
|
||||
create: (_) => RowBloc(
|
||||
fieldController: fieldController,
|
||||
rowId: rowId,
|
||||
@ -74,6 +74,14 @@ class GridRow extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (!editable) {
|
||||
rowContent = IgnorePointer(
|
||||
child: rowContent,
|
||||
);
|
||||
}
|
||||
|
||||
return rowContent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -79,13 +79,19 @@ class DatabaseTabBarView extends StatelessWidget {
|
||||
..add(const DatabaseTabBarEvent.initial()),
|
||||
),
|
||||
BlocProvider<ViewBloc>(
|
||||
create: (_) => ViewBloc(view: view)..add(const ViewEvent.initial()),
|
||||
create: (_) => ViewBloc(view: view)
|
||||
..add(
|
||||
const ViewEvent.initial(),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
|
||||
builder: (_, state) {
|
||||
builder: (innerContext, state) {
|
||||
final layout = state.tabBars[state.selectedIndex].layout;
|
||||
return Column(
|
||||
final isLocked =
|
||||
context.read<ViewBloc?>()?.state.view.isLocked ?? false;
|
||||
|
||||
final Widget child = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (UniversalPlatform.isMobile) const VSpace(12),
|
||||
@ -99,9 +105,17 @@ class DatabaseTabBarView extends StatelessWidget {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return UniversalPlatform.isDesktop
|
||||
Widget child = UniversalPlatform.isDesktop
|
||||
? const TabBarHeader()
|
||||
: const MobileTabBarHeader();
|
||||
|
||||
if (innerContext.watch<ViewBloc>().state.view.isLocked) {
|
||||
child = IgnorePointer(
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
},
|
||||
),
|
||||
pageSettingBarExtensionFromState(context, state),
|
||||
@ -111,6 +125,12 @@ class DatabaseTabBarView extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (isLocked) {
|
||||
return IgnorePointer(child: child);
|
||||
}
|
||||
|
||||
return child;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@ -16,6 +16,7 @@ import 'package:appflowy/workspace/application/action_navigation/action_navigati
|
||||
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
|
||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@ -63,6 +64,7 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
documentBloc.close();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -82,31 +84,56 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
providers: [
|
||||
BlocProvider.value(value: getIt<ActionNavigationBloc>()),
|
||||
BlocProvider.value(value: documentBloc),
|
||||
BlocProvider.value(
|
||||
value: ViewLockStatusBloc(view: widget.view)
|
||||
..add(
|
||||
ViewLockStatusEvent.initial(),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<DocumentBloc, DocumentState>(
|
||||
buildWhen: shouldRebuildDocument,
|
||||
builder: (context, state) {
|
||||
if (state.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
child: BlocConsumer<ViewLockStatusBloc, ViewLockStatusState>(
|
||||
listenWhen: (prev, curr) => curr.isLocked != prev.isLocked,
|
||||
listener: (context, lockStatusState) {
|
||||
if (lockStatusState.isLoadingLockStatus) {
|
||||
return;
|
||||
}
|
||||
editorState?.editable = !lockStatusState.isLocked;
|
||||
},
|
||||
builder: (context, lockStatusState) {
|
||||
return BlocBuilder<DocumentBloc, DocumentState>(
|
||||
buildWhen: shouldRebuildDocument,
|
||||
builder: (context, state) {
|
||||
if (state.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
}
|
||||
|
||||
final editorState = state.editorState;
|
||||
this.editorState = editorState;
|
||||
final error = state.error;
|
||||
if (error != null || editorState == null) {
|
||||
Log.error(error);
|
||||
return Center(child: AppFlowyErrorPage(error: error));
|
||||
}
|
||||
final editorState = state.editorState;
|
||||
this.editorState = editorState;
|
||||
final error = state.error;
|
||||
if (error != null || editorState == null) {
|
||||
Log.error(error);
|
||||
return Center(child: AppFlowyErrorPage(error: error));
|
||||
}
|
||||
|
||||
if (state.forceClose) {
|
||||
widget.onDeleted();
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (state.forceClose) {
|
||||
widget.onDeleted();
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return BlocListener<ActionNavigationBloc, ActionNavigationState>(
|
||||
listenWhen: (_, curr) => curr.action != null,
|
||||
listener: onNotificationAction,
|
||||
child: buildEditorPage(context, state),
|
||||
return BlocListener<ViewLockStatusBloc, ViewLockStatusState>(
|
||||
listener: (context, state) {
|
||||
editorState.editable = !state.isLocked;
|
||||
},
|
||||
child:
|
||||
BlocListener<ActionNavigationBloc, ActionNavigationState>(
|
||||
listenWhen: (_, curr) => curr.action != null,
|
||||
listener: onNotificationAction,
|
||||
child: buildEditorPage(context, state),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@ -14,6 +14,7 @@ import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@ -315,11 +316,13 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
|
||||
);
|
||||
|
||||
final isViewDeleted = context.read<DocumentBloc>().state.isDeleted;
|
||||
final isLocked =
|
||||
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
|
||||
final editor = Directionality(
|
||||
textDirection: textDirection,
|
||||
child: AppFlowyEditor(
|
||||
editorState: widget.editorState,
|
||||
editable: !isViewDeleted,
|
||||
editable: !isViewDeleted && !isLocked,
|
||||
editorScrollController: editorScrollController,
|
||||
// setup the auto focus parameters
|
||||
autoFocus: widget.autoFocus ?? autoFocus,
|
||||
|
||||
@ -71,6 +71,11 @@ extension ViewExtension on ViewPB {
|
||||
name.isEmpty ? LocaleKeys.menuAppHeader_defaultNewPageName.tr() : name;
|
||||
|
||||
bool get isDocument => pluginType == PluginType.document;
|
||||
bool get isDatabase => [
|
||||
PluginType.grid,
|
||||
PluginType.board,
|
||||
PluginType.calendar,
|
||||
].contains(pluginType);
|
||||
|
||||
Widget defaultIcon({Size? size}) => FlowySvg(
|
||||
switch (layout) {
|
||||
|
||||
@ -0,0 +1,123 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
part 'view_lock_status_bloc.freezed.dart';
|
||||
|
||||
class ViewLockStatusBloc
|
||||
extends Bloc<ViewLockStatusEvent, ViewLockStatusState> {
|
||||
ViewLockStatusBloc({
|
||||
required this.view,
|
||||
}) : viewBackendSvc = ViewBackendService(),
|
||||
listener = ViewListener(viewId: view.id),
|
||||
super(ViewLockStatusState.init(view)) {
|
||||
on<ViewLockStatusEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
listener.start(
|
||||
onViewUpdated: (view) async {
|
||||
add(ViewLockStatusEvent.updateLockStatus(view.isLocked));
|
||||
},
|
||||
);
|
||||
|
||||
final result = await ViewBackendService.getView(view.id);
|
||||
final latestView = result.fold(
|
||||
(view) => view,
|
||||
(_) => view,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
view: latestView,
|
||||
isLocked: latestView.isLocked,
|
||||
isLoadingLockStatus: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
lock: () async {
|
||||
final result = await ViewBackendService.lockView(view.id);
|
||||
final isLocked = result.fold(
|
||||
(_) => true,
|
||||
(_) => false,
|
||||
);
|
||||
add(
|
||||
ViewLockStatusEvent.updateLockStatus(
|
||||
isLocked,
|
||||
),
|
||||
);
|
||||
},
|
||||
unlock: () async {
|
||||
final result = await ViewBackendService.unlockView(view.id);
|
||||
final isLocked = result.fold(
|
||||
(_) => false,
|
||||
(_) => true,
|
||||
);
|
||||
add(
|
||||
ViewLockStatusEvent.updateLockStatus(
|
||||
isLocked,
|
||||
lockCounter: state.lockCounter + 1,
|
||||
),
|
||||
);
|
||||
},
|
||||
updateLockStatus: (isLocked, lockCounter) {
|
||||
state.view.freeze();
|
||||
final updatedView = state.view.rebuild(
|
||||
(update) => update.isLocked = isLocked,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
view: updatedView,
|
||||
isLocked: isLocked,
|
||||
lockCounter: lockCounter ?? state.lockCounter,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final ViewPB view;
|
||||
final ViewBackendService viewBackendSvc;
|
||||
final ViewListener listener;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await listener.stop();
|
||||
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ViewLockStatusEvent with _$ViewLockStatusEvent {
|
||||
const factory ViewLockStatusEvent.initial() = Initial;
|
||||
const factory ViewLockStatusEvent.lock() = Lock;
|
||||
const factory ViewLockStatusEvent.unlock() = Unlock;
|
||||
const factory ViewLockStatusEvent.updateLockStatus(
|
||||
bool isLocked, {
|
||||
int? lockCounter,
|
||||
}) = UpdateLockStatus;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ViewLockStatusState with _$ViewLockStatusState {
|
||||
const factory ViewLockStatusState({
|
||||
required ViewPB view,
|
||||
required bool isLocked,
|
||||
required int lockCounter,
|
||||
@Default(true) bool isLoadingLockStatus,
|
||||
}) = _ViewLockStatusState;
|
||||
|
||||
factory ViewLockStatusState.init(ViewPB view) => ViewLockStatusState(
|
||||
view: view,
|
||||
isLocked: false,
|
||||
lockCounter: 0,
|
||||
);
|
||||
}
|
||||
@ -392,4 +392,14 @@ class ViewBackendService {
|
||||
|
||||
return (publishedPages.isNotEmpty, publishedPages);
|
||||
}
|
||||
|
||||
static Future<FlowyResult<void, FlowyError>> lockView(String viewId) async {
|
||||
final payload = ViewIdPB()..value = viewId;
|
||||
return FolderEventLockView(payload).send();
|
||||
}
|
||||
|
||||
static Future<FlowyResult<void, FlowyError>> unlockView(String viewId) async {
|
||||
final payload = ViewIdPB()..value = viewId;
|
||||
return FolderEventUnlockView(payload).send();
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,14 @@ enum ViewMoreActionType {
|
||||
divider,
|
||||
lastModified,
|
||||
created,
|
||||
lockPage;
|
||||
|
||||
static const disableInLockedView = [
|
||||
delete,
|
||||
rename,
|
||||
moveTo,
|
||||
changeIcon,
|
||||
];
|
||||
}
|
||||
|
||||
extension ViewMoreActionTypeExtension on ViewMoreActionType {
|
||||
@ -42,6 +50,8 @@ extension ViewMoreActionTypeExtension on ViewMoreActionType {
|
||||
return LocaleKeys.disclosureAction_changeIcon.tr();
|
||||
case ViewMoreActionType.collapseAllPages:
|
||||
return LocaleKeys.disclosureAction_collapseAllPages.tr();
|
||||
case ViewMoreActionType.lockPage:
|
||||
return LocaleKeys.disclosureAction_lockPage.tr();
|
||||
case ViewMoreActionType.divider:
|
||||
case ViewMoreActionType.lastModified:
|
||||
case ViewMoreActionType.created:
|
||||
@ -69,6 +79,8 @@ extension ViewMoreActionTypeExtension on ViewMoreActionType {
|
||||
return FlowySvgs.change_icon_s;
|
||||
case ViewMoreActionType.collapseAllPages:
|
||||
return FlowySvgs.collapse_all_page_s;
|
||||
case ViewMoreActionType.lockPage:
|
||||
return FlowySvgs.lock_page_s;
|
||||
case ViewMoreActionType.divider:
|
||||
case ViewMoreActionType.lastModified:
|
||||
case ViewMoreActionType.copyLink:
|
||||
@ -92,6 +104,7 @@ extension ViewMoreActionTypeExtension on ViewMoreActionType {
|
||||
case ViewMoreActionType.delete:
|
||||
case ViewMoreActionType.lastModified:
|
||||
case ViewMoreActionType.created:
|
||||
case ViewMoreActionType.lockPage:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
@ -634,7 +635,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
||||
? RawEmojiIconWidget(emoji: iconData, emojiSize: 16.0)
|
||||
: Opacity(opacity: 0.6, child: widget.view.defaultIcon());
|
||||
|
||||
return AppFlowyPopover(
|
||||
final Widget child = AppFlowyPopover(
|
||||
offset: const Offset(20, 0),
|
||||
controller: controller,
|
||||
direction: PopoverDirection.rightWithCenterAligned,
|
||||
@ -669,6 +670,14 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (widget.view.isLocked) {
|
||||
return LockPageButtonWrapper(
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
// > button or · button
|
||||
|
||||
@ -5,6 +5,7 @@ import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/move_to/move_page_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
@ -54,19 +55,22 @@ class ViewMoreActionPopover extends StatelessWidget {
|
||||
|
||||
List<ViewMoreActionTypeWrapper> _buildActionTypeWrappers() {
|
||||
final actionTypes = _buildActionTypes();
|
||||
return actionTypes
|
||||
.map(
|
||||
(e) => ViewMoreActionTypeWrapper(e, view, (controller, data) {
|
||||
onEditing(false);
|
||||
onAction(e, data);
|
||||
bool enableClose = true;
|
||||
if (data is SelectedEmojiIconResult) {
|
||||
if (data.keepOpen) enableClose = false;
|
||||
}
|
||||
if (enableClose) controller.close();
|
||||
}),
|
||||
)
|
||||
.toList();
|
||||
return actionTypes.map(
|
||||
(e) {
|
||||
final actionWrapper =
|
||||
ViewMoreActionTypeWrapper(e, view, (controller, data) {
|
||||
onEditing(false);
|
||||
onAction(e, data);
|
||||
bool enableClose = true;
|
||||
if (data is SelectedEmojiIconResult) {
|
||||
if (data.keepOpen) enableClose = false;
|
||||
}
|
||||
if (enableClose) controller.close();
|
||||
});
|
||||
|
||||
return actionWrapper;
|
||||
},
|
||||
).toList();
|
||||
}
|
||||
|
||||
List<ViewMoreActionType> _buildActionTypes() {
|
||||
@ -144,19 +148,30 @@ class ViewMoreActionTypeWrapper extends CustomActionCell {
|
||||
PopoverController controller,
|
||||
PopoverMutex? mutex,
|
||||
) {
|
||||
Widget child;
|
||||
|
||||
if (inner == ViewMoreActionType.divider) {
|
||||
return _buildDivider();
|
||||
child = _buildDivider();
|
||||
} else if (inner == ViewMoreActionType.lastModified) {
|
||||
return _buildLastModified(context);
|
||||
child = _buildLastModified(context);
|
||||
} else if (inner == ViewMoreActionType.created) {
|
||||
return _buildCreated(context);
|
||||
child = _buildCreated(context);
|
||||
} else if (inner == ViewMoreActionType.changeIcon) {
|
||||
return _buildEmojiActionButton(context, controller);
|
||||
child = _buildEmojiActionButton(context, controller);
|
||||
} else if (inner == ViewMoreActionType.moveTo) {
|
||||
return _buildMoveToActionButton(context, controller);
|
||||
child = _buildMoveToActionButton(context, controller);
|
||||
} else {
|
||||
child = _buildNormalActionButton(context, controller);
|
||||
}
|
||||
|
||||
return _buildNormalActionButton(context, controller);
|
||||
if (ViewMoreActionType.disableInLockedView.contains(inner) &&
|
||||
sourceView.isLocked) {
|
||||
child = LockPageButtonWrapper(
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
Widget _buildNormalActionButton(
|
||||
|
||||
@ -60,7 +60,7 @@ class _AccountDeletionButtonState extends State<AccountDeletionButton> {
|
||||
const VSpace(8),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
Expanded(
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.newSettings_myAccount_deleteAccount_description.tr(),
|
||||
fontSize: 12.0,
|
||||
@ -69,7 +69,6 @@ class _AccountDeletionButtonState extends State<AccountDeletionButton> {
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
const HSpace(32),
|
||||
FlowyTextButton(
|
||||
LocaleKeys.button_deleteAccount.tr(),
|
||||
constraints: const BoxConstraints(minHeight: 32),
|
||||
|
||||
@ -5,10 +5,12 @@ import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
@ -62,18 +64,23 @@ class _MoreViewActionsState extends State<MoreViewActions> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPopup(ViewInfoState state) {
|
||||
Widget _buildPopup(ViewInfoState viewInfoState) {
|
||||
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
|
||||
final userProfile = userWorkspaceBloc.userProfile;
|
||||
final workspaceId =
|
||||
userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? '';
|
||||
final actions = _buildActions(state);
|
||||
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (_) =>
|
||||
ViewBloc(view: widget.view)..add(const ViewEvent.initial()),
|
||||
create: (_) => ViewBloc(view: widget.view)
|
||||
..add(
|
||||
const ViewEvent.initial(),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) => ViewLockStatusBloc(view: widget.view)
|
||||
..add(ViewLockStatusEvent.initial()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => SpaceBloc(
|
||||
@ -84,27 +91,36 @@ class _MoreViewActionsState extends State<MoreViewActions> {
|
||||
),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<SpaceBloc, SpaceState>(
|
||||
builder: (context, state) {
|
||||
if (state.spaces.isEmpty &&
|
||||
userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
child: BlocBuilder<ViewBloc, ViewState>(
|
||||
builder: (context, viewState) {
|
||||
return BlocBuilder<SpaceBloc, SpaceState>(
|
||||
builder: (context, state) {
|
||||
if (state.spaces.isEmpty &&
|
||||
userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
key: ValueKey(state.spaces.hashCode),
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: actions.length,
|
||||
physics: StyledScrollPhysics(),
|
||||
itemBuilder: (_, index) => actions[index],
|
||||
final actions = _buildActions(
|
||||
context,
|
||||
viewInfoState,
|
||||
);
|
||||
return ListView.builder(
|
||||
key: ValueKey(state.spaces.hashCode),
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: actions.length,
|
||||
physics: StyledScrollPhysics(),
|
||||
itemBuilder: (_, index) => actions[index],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildActions(ViewInfoState state) {
|
||||
List<Widget> _buildActions(BuildContext context, ViewInfoState state) {
|
||||
final view = context.watch<ViewLockStatusBloc>().state.view;
|
||||
final appearanceSettings = context.watch<AppearanceSettingsCubit>().state;
|
||||
final dateFormat = appearanceSettings.dateFormat;
|
||||
final timeFormat = appearanceSettings.timeFormat;
|
||||
@ -122,14 +138,24 @@ class _MoreViewActionsState extends State<MoreViewActions> {
|
||||
const FontSizeAction(),
|
||||
ViewAction(
|
||||
type: ViewMoreActionType.divider,
|
||||
view: widget.view,
|
||||
view: view,
|
||||
mutex: popoverMutex,
|
||||
),
|
||||
],
|
||||
if (widget.view.isDocument || widget.view.isDatabase) ...[
|
||||
LockPageAction(
|
||||
view: view,
|
||||
),
|
||||
ViewAction(
|
||||
type: ViewMoreActionType.divider,
|
||||
view: view,
|
||||
mutex: popoverMutex,
|
||||
),
|
||||
],
|
||||
...viewMoreActionTypes.map(
|
||||
(type) => ViewAction(
|
||||
type: type,
|
||||
view: widget.view,
|
||||
view: view,
|
||||
mutex: popoverMutex,
|
||||
),
|
||||
),
|
||||
|
||||
@ -0,0 +1,119 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class LockPageAction extends StatefulWidget {
|
||||
const LockPageAction({
|
||||
super.key,
|
||||
required this.view,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
@override
|
||||
State<LockPageAction> createState() => _LockPageActionState();
|
||||
}
|
||||
|
||||
class _LockPageActionState extends State<LockPageAction> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => ViewLockStatusBloc(view: widget.view)
|
||||
..add(
|
||||
ViewLockStatusEvent.initial(),
|
||||
),
|
||||
child: BlocBuilder<ViewLockStatusBloc, ViewLockStatusState>(
|
||||
builder: (context, state) {
|
||||
return _buildTextButton(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextButton(
|
||||
BuildContext context,
|
||||
) {
|
||||
return Container(
|
||||
height: 34,
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
child: FlowyIconTextButton(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||
onTap: () => _toggle(context),
|
||||
leftIconBuilder: (onHover) => FlowySvg(
|
||||
FlowySvgs.lock_page_s,
|
||||
size: const Size.square(16.0),
|
||||
),
|
||||
iconPadding: 10.0,
|
||||
textBuilder: (onHover) => FlowyText(
|
||||
LocaleKeys.disclosureAction_lockPage.tr(),
|
||||
figmaLineHeight: 18.0,
|
||||
),
|
||||
rightIconBuilder: (_) => _buildSwitch(
|
||||
context,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSwitch(BuildContext context) {
|
||||
final lockState = context.read<ViewLockStatusBloc>().state;
|
||||
if (lockState.isLoadingLockStatus) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: 30,
|
||||
height: 20,
|
||||
margin: const EdgeInsets.only(right: 6),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fill,
|
||||
child: CupertinoSwitch(
|
||||
value: lockState.isLocked,
|
||||
activeTrackColor: Theme.of(context).colorScheme.primary,
|
||||
onChanged: (_) => _toggle(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _toggle(BuildContext context) async {
|
||||
final isLocked = context.read<ViewLockStatusBloc>().state.isLocked;
|
||||
|
||||
context.read<ViewLockStatusBloc>().add(
|
||||
isLocked ? ViewLockStatusEvent.unlock() : ViewLockStatusEvent.lock(),
|
||||
);
|
||||
|
||||
Log.info('update page(${widget.view.id}) lock status: $isLocked');
|
||||
}
|
||||
}
|
||||
|
||||
class LockPageButtonWrapper extends StatelessWidget {
|
||||
const LockPageButtonWrapper({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyTooltip(
|
||||
message: LocaleKeys.lockPage_lockedOperationTooltip.tr(),
|
||||
child: IgnorePointer(
|
||||
child: Opacity(
|
||||
opacity: 0.5,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -5,10 +5,12 @@ import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view_title/view_title_bar_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view_title/view_title_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -29,8 +31,14 @@ class ViewTitleBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => ViewTitleBarBloc(view: view),
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(create: (_) => ViewTitleBarBloc(view: view)),
|
||||
BlocProvider(
|
||||
create: (_) => ViewLockStatusBloc(view: view)
|
||||
..add(const ViewLockStatusEvent.initial()),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<ViewTitleBarBloc, ViewTitleBarState>(
|
||||
builder: (context, state) {
|
||||
final ancestors = state.ancestors;
|
||||
@ -42,11 +50,14 @@ class ViewTitleBar extends StatelessWidget {
|
||||
child: SizedBox(
|
||||
height: 24,
|
||||
child: Row(
|
||||
children: _buildViewTitles(
|
||||
context,
|
||||
ancestors,
|
||||
state.isDeleted,
|
||||
),
|
||||
children: [
|
||||
..._buildViewTitles(
|
||||
context,
|
||||
ancestors,
|
||||
state.isDeleted,
|
||||
),
|
||||
_buildLockPageStatus(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -55,6 +66,30 @@ class ViewTitleBar extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLockPageStatus(BuildContext context) {
|
||||
return BlocConsumer<ViewLockStatusBloc, ViewLockStatusState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.isLoadingLockStatus == current.isLoadingLockStatus &&
|
||||
current.isLoadingLockStatus == false,
|
||||
listener: (context, state) {
|
||||
if (state.isLocked) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.lockPage_pageLockedToast.tr(),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state.isLocked) {
|
||||
return _Lock();
|
||||
} else if (!state.isLocked && state.lockCounter > 0) {
|
||||
return _ReLock();
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildViewTitles(
|
||||
BuildContext context,
|
||||
List<ViewPB> views,
|
||||
@ -98,7 +133,7 @@ class ViewTitleBar extends StatelessWidget {
|
||||
message: view.name,
|
||||
child: ViewTitle(
|
||||
view: view,
|
||||
behavior: i == views.length - 1
|
||||
behavior: i == views.length - 1 && !view.isLocked
|
||||
? ViewTitleBehavior.editable // only the last one is editable
|
||||
: ViewTitleBehavior.uneditable, // others are not editable
|
||||
onUpdated: () {
|
||||
@ -350,3 +385,78 @@ class _ViewTitleState extends State<ViewTitle> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Lock extends StatelessWidget {
|
||||
const _Lock();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = const Color(0xFFD95A0B);
|
||||
return FlowyTooltip(
|
||||
message: LocaleKeys.lockPage_lockTooltip.tr(),
|
||||
child: DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(color: color),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
child: FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 4.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
iconPadding: 4.0,
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.lockPage_lockPage.tr(),
|
||||
color: color,
|
||||
fontSize: 12.0,
|
||||
),
|
||||
hoverColor: color.withValues(alpha: 0.1),
|
||||
leftIcon: FlowySvg(FlowySvgs.lock_page_s, color: color),
|
||||
onTap: () => context.read<ViewLockStatusBloc>().add(
|
||||
const ViewLockStatusEvent.unlock(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ReLock extends StatelessWidget {
|
||||
const _ReLock();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final iconColor = const Color(0xFF8F959E);
|
||||
return DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(color: iconColor),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
child: FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 4.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
iconPadding: 4.0,
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.lockPage_reLockPage.tr(),
|
||||
fontSize: 12.0,
|
||||
),
|
||||
leftIcon: FlowySvg(
|
||||
FlowySvgs.unlock_page_s,
|
||||
color: iconColor,
|
||||
blendMode: null,
|
||||
),
|
||||
onTap: () => context.read<ViewLockStatusBloc>().add(
|
||||
const ViewLockStatusEvent.lock(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,8 +90,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "55c457f"
|
||||
resolved-ref: "55c457f472ed997906bd77142ef94bb7f66cc629"
|
||||
ref: "9b5c461"
|
||||
resolved-ref: "9b5c46153a769affc9e5d85ff91115796e97bce8"
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||
source: git
|
||||
version: "5.0.0"
|
||||
|
||||
@ -4,7 +4,7 @@ description: Bring projects, wikis, and teams together with AI. AppFlowy is an
|
||||
your data. The best open source alternative to Notion.
|
||||
publish_to: "none"
|
||||
|
||||
version: 0.8.2
|
||||
version: 0.8.4
|
||||
|
||||
environment:
|
||||
flutter: ">=3.27.4"
|
||||
@ -178,7 +178,7 @@ dependency_overrides:
|
||||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
ref: "55c457f"
|
||||
ref: "9b5c461"
|
||||
|
||||
appflowy_editor_plugins:
|
||||
git:
|
||||
|
||||
4
frontend/resources/flowy_icons/16x/lock_page.svg
Normal file
4
frontend/resources/flowy_icons/16x/lock_page.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.75 10.5C1.75 8.73225 1.75 7.84838 2.29918 7.29919C2.84835 6.75 3.73223 6.75 5.5 6.75H10.5C12.2678 6.75 13.1516 6.75 13.7008 7.29919C14.25 7.84838 14.25 8.73225 14.25 10.5C14.25 12.2678 14.25 13.1516 13.7008 13.7008C13.1516 14.25 12.2678 14.25 10.5 14.25H5.5C3.73223 14.25 2.84835 14.25 2.29918 13.7008C1.75 13.1516 1.75 12.2678 1.75 10.5Z" stroke="#171711"/>
|
||||
<path d="M4.25 6.75V5.5C4.25 3.42893 5.92893 1.75 8 1.75C10.0711 1.75 11.75 3.42893 11.75 5.5V6.75" stroke="#171711" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 616 B |
4
frontend/resources/flowy_icons/16x/unlock_page.svg
Normal file
4
frontend/resources/flowy_icons/16x/unlock_page.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.75 10.5C1.75 8.73225 1.75 7.84838 2.29918 7.29919C2.84835 6.75 3.73223 6.75 5.5 6.75H10.5C12.2677 6.75 13.1516 6.75 13.7008 7.29919C14.25 7.84838 14.25 8.73225 14.25 10.5C14.25 12.2677 14.25 13.1516 13.7008 13.7008C13.1516 14.25 12.2677 14.25 10.5 14.25H5.5C3.73223 14.25 2.84835 14.25 2.29918 13.7008C1.75 13.1516 1.75 12.2677 1.75 10.5Z" fill="#8F959E" stroke="#8F959E"/>
|
||||
<path d="M4.25 6.75V5.5C4.25 3.42893 5.92893 1.75 8 1.75C9.11064 1.75 10.1085 2.23281 10.7951 3" stroke="#8F959E" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 628 B |
@ -174,7 +174,8 @@
|
||||
"changeIcon": "Change icon",
|
||||
"collapseAllPages": "Collapse all subpages",
|
||||
"movePageTo": "Move page to",
|
||||
"move": "Move"
|
||||
"move": "Move",
|
||||
"lockPage": "Lock page"
|
||||
},
|
||||
"blankPageTitle": "Blank page",
|
||||
"newPageText": "New page",
|
||||
@ -3096,5 +3097,12 @@
|
||||
"settingsUpdateDescription": "Current version: {currentVersion} (Official build) → {newVersion}",
|
||||
"settingsUpdateButton": "Update now",
|
||||
"settingsUpdateWhatsNew": "What's new"
|
||||
},
|
||||
"lockPage": {
|
||||
"lockPage": "Locked",
|
||||
"reLockPage": "Re-lock",
|
||||
"lockTooltip": "Page locked to prevent accidental editing. Click to unlock.",
|
||||
"pageLockedToast": "Page locked. Editing is disabled until someone unlocks it.",
|
||||
"lockedOperationTooltip": "Page locked to prevent accidental editing."
|
||||
}
|
||||
}
|
||||
|
||||
34
frontend/rust-lib/Cargo.lock
generated
34
frontend/rust-lib/Cargo.lock
generated
@ -898,7 +898,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=98463cc#98463cc764da69ef58e5cd894d1885ef4990bf9e"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68fd31892b2170568cae3f01206986fddd6d0988#68fd31892b2170568cae3f01206986fddd6d0988"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@ -923,7 +923,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-database"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=98463cc#98463cc764da69ef58e5cd894d1885ef4990bf9e"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68fd31892b2170568cae3f01206986fddd6d0988#68fd31892b2170568cae3f01206986fddd6d0988"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -963,7 +963,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-document"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=98463cc#98463cc764da69ef58e5cd894d1885ef4990bf9e"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68fd31892b2170568cae3f01206986fddd6d0988#68fd31892b2170568cae3f01206986fddd6d0988"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@ -984,7 +984,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-entity"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=98463cc#98463cc764da69ef58e5cd894d1885ef4990bf9e"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68fd31892b2170568cae3f01206986fddd6d0988#68fd31892b2170568cae3f01206986fddd6d0988"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@ -1004,7 +1004,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-folder"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=98463cc#98463cc764da69ef58e5cd894d1885ef4990bf9e"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68fd31892b2170568cae3f01206986fddd6d0988#68fd31892b2170568cae3f01206986fddd6d0988"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@ -1026,7 +1026,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-importer"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=98463cc#98463cc764da69ef58e5cd894d1885ef4990bf9e"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68fd31892b2170568cae3f01206986fddd6d0988#68fd31892b2170568cae3f01206986fddd6d0988"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@ -1090,7 +1090,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-plugins"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=98463cc#98463cc764da69ef58e5cd894d1885ef4990bf9e"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68fd31892b2170568cae3f01206986fddd6d0988#68fd31892b2170568cae3f01206986fddd6d0988"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@ -1170,7 +1170,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-user"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=98463cc#98463cc764da69ef58e5cd894d1885ef4990bf9e"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68fd31892b2170568cae3f01206986fddd6d0988#68fd31892b2170568cae3f01206986fddd6d0988"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@ -1400,7 +1400,7 @@ dependencies = [
|
||||
"cssparser-macros",
|
||||
"dtoa-short",
|
||||
"itoa",
|
||||
"phf 0.11.2",
|
||||
"phf 0.8.0",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
@ -4624,7 +4624,7 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
|
||||
dependencies = [
|
||||
"phf_macros 0.8.0",
|
||||
"phf_macros",
|
||||
"phf_shared 0.8.0",
|
||||
"proc-macro-hack",
|
||||
]
|
||||
@ -4644,7 +4644,6 @@ version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
|
||||
dependencies = [
|
||||
"phf_macros 0.11.3",
|
||||
"phf_shared 0.11.2",
|
||||
]
|
||||
|
||||
@ -4712,19 +4711,6 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
|
||||
dependencies = [
|
||||
"phf_generator 0.11.2",
|
||||
"phf_shared 0.11.2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.94",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.8.0"
|
||||
|
||||
@ -139,14 +139,14 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120
|
||||
# To switch to the local path, run:
|
||||
# scripts/tool/update_collab_source.sh
|
||||
# ⚠️⚠️⚠️️
|
||||
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "98463cc" }
|
||||
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "98463cc" }
|
||||
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "98463cc" }
|
||||
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "98463cc" }
|
||||
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "98463cc" }
|
||||
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "98463cc" }
|
||||
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "98463cc" }
|
||||
collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "98463cc" }
|
||||
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68fd31892b2170568cae3f01206986fddd6d0988" }
|
||||
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68fd31892b2170568cae3f01206986fddd6d0988" }
|
||||
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68fd31892b2170568cae3f01206986fddd6d0988" }
|
||||
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68fd31892b2170568cae3f01206986fddd6d0988" }
|
||||
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68fd31892b2170568cae3f01206986fddd6d0988" }
|
||||
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68fd31892b2170568cae3f01206986fddd6d0988" }
|
||||
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68fd31892b2170568cae3f01206986fddd6d0988" }
|
||||
collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68fd31892b2170568cae3f01206986fddd6d0988" }
|
||||
|
||||
# Working directory: frontend
|
||||
# To update the commit ID, run:
|
||||
|
||||
@ -365,6 +365,9 @@ pub enum ErrorCode {
|
||||
|
||||
#[error("AI Max Required")]
|
||||
AIMaxRequired = 125,
|
||||
|
||||
#[error("View is locked")]
|
||||
ViewIsLocked = 126,
|
||||
}
|
||||
|
||||
impl ErrorCode {
|
||||
|
||||
@ -149,6 +149,8 @@ impl FlowyError {
|
||||
static_flowy_error!(local_ai_unavailable, ErrorCode::LocalAIUnavailable);
|
||||
static_flowy_error!(response_timeout, ErrorCode::ResponseTimeout);
|
||||
static_flowy_error!(file_storage_limit, ErrorCode::FileStorageLimitExceeded);
|
||||
|
||||
static_flowy_error!(view_is_locked, ErrorCode::ViewIsLocked);
|
||||
}
|
||||
|
||||
impl std::convert::From<ErrorCode> for FlowyError {
|
||||
|
||||
@ -72,6 +72,11 @@ pub struct ViewPB {
|
||||
// user_id
|
||||
#[pb(index = 12, one_of)]
|
||||
pub last_edited_by: Option<i64>,
|
||||
|
||||
// is_locked
|
||||
// If true, the view is locked and cannot be edited.
|
||||
#[pb(index = 13, one_of)]
|
||||
pub is_locked: Option<bool>,
|
||||
}
|
||||
|
||||
pub fn view_pb_without_child_views(view: View) -> ViewPB {
|
||||
@ -88,6 +93,7 @@ pub fn view_pb_without_child_views(view: View) -> ViewPB {
|
||||
created_by: view.created_by,
|
||||
last_edited: view.last_edited_time,
|
||||
last_edited_by: view.last_edited_by,
|
||||
is_locked: view.is_locked,
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,6 +111,7 @@ pub fn view_pb_without_child_views_from_arc(view: Arc<View>) -> ViewPB {
|
||||
created_by: view.created_by,
|
||||
last_edited: view.last_edited_time,
|
||||
last_edited_by: view.last_edited_by,
|
||||
is_locked: view.is_locked,
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,6 +133,7 @@ pub fn view_pb_with_child_views(view: Arc<View>, child_views: Vec<Arc<View>>) ->
|
||||
created_by: view.created_by,
|
||||
last_edited: view.last_edited_time,
|
||||
last_edited_by: view.last_edited_by,
|
||||
is_locked: view.is_locked,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -533,3 +533,25 @@ pub(crate) async fn remove_default_publish_view_handler(
|
||||
folder.remove_default_published_view().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(data, folder))]
|
||||
pub(crate) async fn lock_view_handler(
|
||||
data: AFPluginData<ViewIdPB>,
|
||||
folder: AFPluginState<Weak<FolderManager>>,
|
||||
) -> Result<(), FlowyError> {
|
||||
let folder = upgrade_folder(folder)?;
|
||||
let view_id = data.into_inner().value;
|
||||
folder.lock_view(&view_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(data, folder))]
|
||||
pub(crate) async fn unlock_view_handler(
|
||||
data: AFPluginData<ViewIdPB>,
|
||||
folder: AFPluginState<Weak<FolderManager>>,
|
||||
) -> Result<(), FlowyError> {
|
||||
let folder = upgrade_folder(folder)?;
|
||||
let view_id = data.into_inner().value;
|
||||
folder.unlock_view(&view_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -53,6 +53,8 @@ pub fn init(folder: Weak<FolderManager>) -> AFPlugin {
|
||||
.event(FolderEvent::GetDefaultPublishInfo, get_default_publish_info_handler)
|
||||
.event(FolderEvent::SetDefaultPublishView, set_default_publish_view_handler)
|
||||
.event(FolderEvent::RemoveDefaultPublishView, remove_default_publish_view_handler)
|
||||
.event(FolderEvent::LockView, lock_view_handler)
|
||||
.event(FolderEvent::UnlockView, unlock_view_handler)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
|
||||
@ -220,4 +222,10 @@ pub enum FolderEvent {
|
||||
|
||||
#[event()]
|
||||
RemoveDefaultPublishView = 53,
|
||||
|
||||
#[event(input = "ViewIdPB")]
|
||||
LockView = 54,
|
||||
|
||||
#[event(input = "ViewIdPB")]
|
||||
UnlockView = 55,
|
||||
}
|
||||
|
||||
@ -767,6 +767,11 @@ impl FolderManager {
|
||||
}
|
||||
|
||||
if let Some(view) = folder.get_view(view_id) {
|
||||
// if the view is locked, the view can't be moved to trash
|
||||
if view.is_locked.unwrap_or(false) {
|
||||
return Err(FlowyError::view_is_locked());
|
||||
}
|
||||
|
||||
Self::unfavorite_view_and_decendants(view.clone(), &mut folder);
|
||||
folder.add_trash_view_ids(vec![view_id.to_string()]);
|
||||
drop(folder);
|
||||
@ -840,6 +845,11 @@ impl FolderManager {
|
||||
let from_section = params.from_section;
|
||||
let to_section = params.to_section;
|
||||
let view = self.get_view_pb(&view_id).await?;
|
||||
// if the view is locked, the view can't be moved
|
||||
if view.is_locked.unwrap_or(false) {
|
||||
return Err(FlowyError::view_is_locked());
|
||||
}
|
||||
|
||||
let old_parent_id = view.parent_view_id;
|
||||
if let Some(lock) = self.mutex_folder.load_full() {
|
||||
let mut folder = lock.write().await;
|
||||
@ -863,6 +873,12 @@ impl FolderManager {
|
||||
#[tracing::instrument(level = "trace", skip(self), err)]
|
||||
pub async fn move_view(&self, view_id: &str, from: usize, to: usize) -> FlowyResult<()> {
|
||||
let workspace_id = self.user.workspace_id()?;
|
||||
let view = self.get_view_pb(view_id).await?;
|
||||
// if the view is locked, the view can't be moved
|
||||
if view.is_locked.unwrap_or(false) {
|
||||
return Err(FlowyError::view_is_locked());
|
||||
}
|
||||
|
||||
if let Some((is_workspace, parent_view_id, child_views)) = self.get_view_relation(view_id).await
|
||||
{
|
||||
// The display parent view is the view that is displayed in the UI
|
||||
@ -952,7 +968,7 @@ impl FolderManager {
|
||||
#[tracing::instrument(level = "trace", skip(self), err)]
|
||||
pub async fn update_view_with_params(&self, params: UpdateViewParams) -> FlowyResult<()> {
|
||||
self
|
||||
.update_view(¶ms.view_id, |update| {
|
||||
.update_view(¶ms.view_id, true, |update| {
|
||||
update
|
||||
.set_name_if_not_none(params.name)
|
||||
.set_desc_if_not_none(params.desc)
|
||||
@ -971,12 +987,34 @@ impl FolderManager {
|
||||
params: UpdateViewIconParams,
|
||||
) -> FlowyResult<()> {
|
||||
self
|
||||
.update_view(¶ms.view_id, |update| {
|
||||
.update_view(¶ms.view_id, true, |update| {
|
||||
update.set_icon(params.icon).done()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Lock the view with the given view id.
|
||||
///
|
||||
/// If the view is locked, it cannot be edited.
|
||||
#[tracing::instrument(level = "debug", skip(self), err)]
|
||||
pub async fn lock_view(&self, view_id: &str) -> FlowyResult<()> {
|
||||
self
|
||||
.update_view(view_id, false, |update| {
|
||||
update.set_page_lock_status(true).done()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Unlock the view with the given view id.
|
||||
#[tracing::instrument(level = "debug", skip(self), err)]
|
||||
pub async fn unlock_view(&self, view_id: &str) -> FlowyResult<()> {
|
||||
self
|
||||
.update_view(view_id, false, |update| {
|
||||
update.set_page_lock_status(false).done()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Duplicate the view with the given view id.
|
||||
///
|
||||
/// Including the view data (icon, cover, extra) and the child views.
|
||||
@ -1791,7 +1829,10 @@ impl FolderManager {
|
||||
}
|
||||
|
||||
/// Update the view with the provided view_id using the specified function.
|
||||
async fn update_view<F>(&self, view_id: &str, f: F) -> FlowyResult<()>
|
||||
///
|
||||
/// If the check_locked is true, it will check the lock status of the view. If the view is locked,
|
||||
/// it will return an error.
|
||||
async fn update_view<F>(&self, view_id: &str, check_locked: bool, f: F) -> FlowyResult<()>
|
||||
where
|
||||
F: FnOnce(ViewUpdate) -> Option<View>,
|
||||
{
|
||||
@ -1801,6 +1842,12 @@ impl FolderManager {
|
||||
Some(lock) => {
|
||||
let mut folder = lock.write().await;
|
||||
let old_view = folder.get_view(view_id);
|
||||
|
||||
// Check if the view is locked
|
||||
if check_locked && old_view.as_ref().and_then(|v| v.is_locked).unwrap_or(false) {
|
||||
return Err(FlowyError::view_is_locked());
|
||||
}
|
||||
|
||||
let new_view = folder.update_view(view_id, f);
|
||||
|
||||
Some((old_view, new_view))
|
||||
|
||||
@ -164,6 +164,7 @@ pub(crate) fn create_view(uid: i64, params: CreateViewParams, layout: ViewLayout
|
||||
last_edited_by: Some(uid),
|
||||
extra: params.extra,
|
||||
children: Default::default(),
|
||||
is_locked: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user