Merge branch 'upstream-main' into feat/tauri-kanban

# Conflicts:
#	frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts
This commit is contained in:
ascarbek 2023-03-17 13:22:05 +06:00
commit 745ee264c8
31 changed files with 276 additions and 157 deletions

View File

@ -159,8 +159,11 @@ class DatabaseController {
); );
} }
Future<Either<Unit, FlowyError>> moveRow(RowPB fromRow, Future<Either<Unit, FlowyError>> moveRow({
{RowPB? toRow, String? groupId}) { required RowPB fromRow,
required String groupId,
RowPB? toRow,
}) {
return _databaseViewBackendSvc.moveRow( return _databaseViewBackendSvc.moveRow(
fromRowId: fromRow.id, fromRowId: fromRow.id,
toGroupId: groupId, toGroupId: groupId,

View File

@ -46,15 +46,13 @@ class DatabaseViewBackendService {
Future<Either<Unit, FlowyError>> moveRow({ Future<Either<Unit, FlowyError>> moveRow({
required String fromRowId, required String fromRowId,
required String? toGroupId, required String toGroupId,
required String? toRowId, String? toRowId,
}) { }) {
var payload = MoveGroupRowPayloadPB.create() var payload = MoveGroupRowPayloadPB.create()
..viewId = viewId ..viewId = viewId
..fromRowId = fromRowId; ..fromRowId = fromRowId
if (toGroupId != null) { ..toGroupId = toGroupId;
payload.toGroupId = toGroupId;
}
if (toRowId != null) { if (toRowId != null) {
payload.toRowId = toRowId; payload.toRowId = toRowId;

View File

@ -54,7 +54,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
final toRow = groupControllers[groupId]?.rowAtIndex(toIndex); final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);
if (fromRow != null) { if (fromRow != null) {
_databaseController.moveRow( _databaseController.moveRow(
fromRow, fromRow: fromRow,
toRow: toRow, toRow: toRow,
groupId: groupId, groupId: groupId,
); );
@ -70,7 +70,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex); final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex);
if (fromRow != null) { if (fromRow != null) {
_databaseController.moveRow( _databaseController.moveRow(
fromRow, fromRow: fromRow,
toRow: toRow, toRow: toRow,
groupId: toGroupId, groupId: toGroupId,
); );

View File

@ -65,6 +65,7 @@ class _SettingButtonState extends State<_SettingButton> {
return AppFlowyPopover( return AppFlowyPopover(
controller: popoverController, controller: popoverController,
direction: PopoverDirection.leftWithTopAligned, direction: PopoverDirection.leftWithTopAligned,
offset: const Offset(-8, 0),
triggerActions: PopoverTriggerFlags.none, triggerActions: PopoverTriggerFlags.none,
constraints: BoxConstraints.loose(const Size(260, 400)), constraints: BoxConstraints.loose(const Size(260, 400)),
margin: EdgeInsets.zero, margin: EdgeInsets.zero,

View File

@ -87,7 +87,7 @@ class _SwitchFieldButton extends StatelessWidget {
asBarrier: true, asBarrier: true,
triggerActions: PopoverTriggerFlags.click, triggerActions: PopoverTriggerFlags.click,
mutex: popoverMutex, mutex: popoverMutex,
offset: const Offset(20, 0), offset: const Offset(8, 0),
popupBuilder: (popOverContext) { popupBuilder: (popOverContext) {
return FieldTypeList(onSelectField: (newFieldType) { return FieldTypeList(onSelectField: (newFieldType) {
context context

View File

@ -80,7 +80,7 @@ class DateTypeOptionWidget extends TypeOptionWidget {
mutex: popoverMutex, mutex: popoverMutex,
asBarrier: true, asBarrier: true,
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(20, 0), offset: const Offset(8, 0),
constraints: BoxConstraints.loose(const Size(460, 440)), constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (popoverContext) { popupBuilder: (popoverContext) {
return DateFormatList( return DateFormatList(
@ -107,7 +107,7 @@ class DateTypeOptionWidget extends TypeOptionWidget {
mutex: popoverMutex, mutex: popoverMutex,
asBarrier: true, asBarrier: true,
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(20, 0), offset: const Offset(8, 0),
constraints: BoxConstraints.loose(const Size(460, 440)), constraints: BoxConstraints.loose(const Size(460, 440)),
popupBuilder: (BuildContext popoverContext) { popupBuilder: (BuildContext popoverContext) {
return TimeFormatList( return TimeFormatList(

View File

@ -77,7 +77,7 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
mutex: popoverMutex, mutex: popoverMutex,
triggerActions: triggerActions:
PopoverTriggerFlags.hover | PopoverTriggerFlags.click, PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(20, 0), offset: const Offset(8, 0),
constraints: BoxConstraints.loose(const Size(460, 440)), constraints: BoxConstraints.loose(const Size(460, 440)),
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: Padding( child: Padding(

View File

@ -203,7 +203,7 @@ class _OptionCellState extends State<_OptionCell> {
return AppFlowyPopover( return AppFlowyPopover(
controller: _popoverController, controller: _popoverController,
mutex: widget.popoverMutex, mutex: widget.popoverMutex,
offset: const Offset(20, 0), offset: const Offset(8, 0),
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
asBarrier: true, asBarrier: true,
constraints: BoxConstraints.loose(const Size(460, 460)), constraints: BoxConstraints.loose(const Size(460, 460)),

View File

@ -176,7 +176,7 @@ class _AddSortButtonState extends State<_AddSortButton> {
mutex: widget.popoverMutex, mutex: widget.popoverMutex,
direction: PopoverDirection.bottomWithLeftAligned, direction: PopoverDirection.bottomWithLeftAligned,
constraints: BoxConstraints.loose(const Size(200, 300)), constraints: BoxConstraints.loose(const Size(200, 300)),
offset: const Offset(0, 10), offset: const Offset(0, 8),
triggerActions: PopoverTriggerFlags.none, triggerActions: PopoverTriggerFlags.none,
asBarrier: true, asBarrier: true,
child: SizedBox( child: SizedBox(

View File

@ -30,20 +30,23 @@ class _FilterButtonState extends State<FilterButton> {
return _wrapPopover( return _wrapPopover(
context, context,
FlowyTextButton( SizedBox(
LocaleKeys.grid_settings_filter.tr(), height: 26,
fontColor: textColor, child: FlowyTextButton(
fillColor: Colors.transparent, LocaleKeys.grid_settings_filter.tr(),
hoverColor: AFThemeExtension.of(context).lightGreyHover, fontColor: textColor,
padding: GridSize.typeOptionContentInsets, fillColor: Colors.transparent,
onPressed: () { hoverColor: AFThemeExtension.of(context).lightGreyHover,
final bloc = context.read<GridFilterMenuBloc>(); padding: GridSize.typeOptionContentInsets,
if (bloc.state.filters.isEmpty) { onPressed: () {
_popoverController.show(); final bloc = context.read<GridFilterMenuBloc>();
} else { if (bloc.state.filters.isEmpty) {
bloc.add(const GridFilterMenuEvent.toggleMenu()); _popoverController.show();
} } else {
}, bloc.add(const GridFilterMenuEvent.toggleMenu());
}
},
),
), ),
); );
}, },
@ -55,7 +58,7 @@ class _FilterButtonState extends State<FilterButton> {
controller: _popoverController, controller: _popoverController,
direction: PopoverDirection.bottomWithLeftAligned, direction: PopoverDirection.bottomWithLeftAligned,
constraints: BoxConstraints.loose(const Size(200, 300)), constraints: BoxConstraints.loose(const Size(200, 300)),
offset: const Offset(0, 10), offset: const Offset(0, 8),
triggerActions: PopoverTriggerFlags.none, triggerActions: PopoverTriggerFlags.none,
child: child, child: child,
popupBuilder: (BuildContext context) { popupBuilder: (BuildContext context) {

View File

@ -109,7 +109,7 @@ class _GridPropertyCellState extends State<_GridPropertyCell> {
return AppFlowyPopover( return AppFlowyPopover(
mutex: widget.popoverMutex, mutex: widget.popoverMutex,
controller: _popoverController, controller: _popoverController,
offset: const Offset(20, 0), offset: const Offset(8, 0),
direction: PopoverDirection.leftWithTopAligned, direction: PopoverDirection.leftWithTopAligned,
constraints: BoxConstraints.loose(const Size(240, 400)), constraints: BoxConstraints.loose(const Size(240, 400)),
triggerActions: PopoverTriggerFlags.none, triggerActions: PopoverTriggerFlags.none,

View File

@ -41,23 +41,26 @@ class _SettingButtonState extends State<SettingButton> {
); );
}, },
builder: (context, settingContext) { builder: (context, settingContext) {
return AppFlowyPopover( return SizedBox(
controller: _popoverController, height: 26,
constraints: BoxConstraints.loose(const Size(260, 400)), child: AppFlowyPopover(
direction: PopoverDirection.bottomWithLeftAligned, controller: _popoverController,
offset: const Offset(0, 10), constraints: BoxConstraints.loose(const Size(260, 400)),
margin: EdgeInsets.zero, direction: PopoverDirection.bottomWithLeftAligned,
triggerActions: PopoverTriggerFlags.none, offset: const Offset(0, 8),
child: FlowyTextButton( margin: EdgeInsets.zero,
LocaleKeys.settings_title.tr(), triggerActions: PopoverTriggerFlags.none,
fillColor: Colors.transparent, child: FlowyTextButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover, LocaleKeys.settings_title.tr(),
padding: GridSize.typeOptionContentInsets, fillColor: Colors.transparent,
onPressed: () => _popoverController.show(), hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.typeOptionContentInsets,
onPressed: () => _popoverController.show(),
),
popupBuilder: (BuildContext context) {
return _GridSettingListPopover(settingContext: settingContext);
},
), ),
popupBuilder: (BuildContext context) {
return _GridSettingListPopover(settingContext: settingContext);
},
); );
}, },
); );

View File

@ -58,7 +58,7 @@ class _SortButtonState extends State<SortButton> {
controller: _popoverController, controller: _popoverController,
direction: PopoverDirection.bottomWithLeftAligned, direction: PopoverDirection.bottomWithLeftAligned,
constraints: BoxConstraints.loose(const Size(200, 300)), constraints: BoxConstraints.loose(const Size(200, 300)),
offset: const Offset(0, 10), offset: const Offset(0, 8),
margin: const EdgeInsets.all(6), margin: const EdgeInsets.all(6),
triggerActions: PopoverTriggerFlags.none, triggerActions: PopoverTriggerFlags.none,
child: child, child: child,

View File

@ -145,7 +145,7 @@ class _ChecklistOptionCellState extends State<_ChecklistOptionCell> {
Widget _wrapPopover(Widget child) { Widget _wrapPopover(Widget child) {
return AppFlowyPopover( return AppFlowyPopover(
controller: _popoverController, controller: _popoverController,
offset: const Offset(20, 0), offset: const Offset(8, 0),
asBarrier: true, asBarrier: true,
constraints: BoxConstraints.loose(const Size(200, 300)), constraints: BoxConstraints.loose(const Size(200, 300)),
mutex: widget.popoverMutex, mutex: widget.popoverMutex,

View File

@ -376,7 +376,7 @@ class _DateTypeOptionButton extends StatelessWidget {
return AppFlowyPopover( return AppFlowyPopover(
mutex: popoverMutex, mutex: popoverMutex,
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(20, 0), offset: const Offset(8, 0),
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
constraints: BoxConstraints.loose(const Size(140, 100)), constraints: BoxConstraints.loose(const Size(140, 100)),
child: Padding( child: Padding(
@ -431,7 +431,7 @@ class _CalDateTimeSettingState extends State<_CalDateTimeSetting> {
AppFlowyPopover( AppFlowyPopover(
mutex: timeSettingPopoverMutex, mutex: timeSettingPopoverMutex,
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(20, 0), offset: const Offset(8, 0),
popupBuilder: (BuildContext context) { popupBuilder: (BuildContext context) {
return DateFormatList( return DateFormatList(
selectedFormat: widget.dateTypeOptionPB.dateFormat, selectedFormat: widget.dateTypeOptionPB.dateFormat,
@ -449,7 +449,7 @@ class _CalDateTimeSettingState extends State<_CalDateTimeSetting> {
AppFlowyPopover( AppFlowyPopover(
mutex: timeSettingPopoverMutex, mutex: timeSettingPopoverMutex,
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(20, 0), offset: const Offset(8, 0),
popupBuilder: (BuildContext context) { popupBuilder: (BuildContext context) {
return TimeFormatList( return TimeFormatList(
selectedFormat: widget.dateTypeOptionPB.timeFormat, selectedFormat: widget.dateTypeOptionPB.timeFormat,

View File

@ -285,7 +285,7 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
); );
return AppFlowyPopover( return AppFlowyPopover(
controller: _popoverController, controller: _popoverController,
offset: const Offset(20, 0), offset: const Offset(8, 0),
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
asBarrier: true, asBarrier: true,
constraints: BoxConstraints.loose(const Size(200, 460)), constraints: BoxConstraints.loose(const Size(200, 460)),

View File

@ -127,7 +127,7 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
constraints: BoxConstraints.loose(const Size(300, 160)), constraints: BoxConstraints.loose(const Size(300, 160)),
direction: PopoverDirection.bottomWithLeftAligned, direction: PopoverDirection.bottomWithLeftAligned,
triggerActions: PopoverTriggerFlags.none, triggerActions: PopoverTriggerFlags.none,
offset: const Offset(0, 20), offset: const Offset(0, 8),
child: SizedBox.expand( child: SizedBox.expand(
child: GestureDetector( child: GestureDetector(
child: Align(alignment: Alignment.centerLeft, child: richText), child: Align(alignment: Alignment.centerLeft, child: richText),
@ -210,7 +210,7 @@ class _EditURLAccessoryState extends State<_EditURLAccessory>
constraints: BoxConstraints.loose(const Size(300, 160)), constraints: BoxConstraints.loose(const Size(300, 160)),
controller: _popoverController, controller: _popoverController,
direction: PopoverDirection.bottomWithLeftAligned, direction: PopoverDirection.bottomWithLeftAligned,
offset: const Offset(0, 20), offset: const Offset(0, 8),
child: svgWidget( child: svgWidget(
"editor/edit", "editor/edit",
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,

View File

@ -12,13 +12,15 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as p;
import 'change_cover_popover.dart'; import 'change_cover_popover.dart';
part 'cover_image_picker_bloc.freezed.dart'; part 'cover_image_picker_bloc.freezed.dart';
class CoverImagePickerBloc class CoverImagePickerBloc
extends Bloc<CoverImagePickerEvent, CoverImagePickerState> { extends Bloc<CoverImagePickerEvent, CoverImagePickerState> {
static const allowedExtensions = ['jpg', 'png', 'jpeg'];
CoverImagePickerBloc() : super(const CoverImagePickerState.initial()) { CoverImagePickerBloc() : super(const CoverImagePickerState.initial()) {
on<CoverImagePickerEvent>( on<CoverImagePickerEvent>(
(event, emit) async { (event, emit) async {
@ -28,7 +30,7 @@ class CoverImagePickerBloc
}, },
urlSubmit: (UrlSubmit urlSubmit) async { urlSubmit: (UrlSubmit urlSubmit) async {
emit(const CoverImagePickerState.loading()); emit(const CoverImagePickerState.loading());
final validateImage = await _validateUrl(urlSubmit.path); final validateImage = await _validateURL(urlSubmit.path);
if (validateImage) { if (validateImage) {
emit(CoverImagePickerState.networkImage(left(urlSubmit.path))); emit(CoverImagePickerState.networkImage(left(urlSubmit.path)));
} else { } else {
@ -86,28 +88,22 @@ class CoverImagePickerBloc
if (state is FileImagePicked) { if (state is FileImagePicked) {
try { try {
final path = state.path; final path = state.path;
final newPath = '$directory/${path.split("\\").last}'; final newPath = p.join(directory, p.split(path).last);
final newFile = await File(path).copy(newPath); final newFile = await File(path).copy(newPath);
imagePaths.add(newFile.path); imagePaths.add(newFile.path);
await prefs.setStringList(kLocalImagesKey, imagePaths);
return imagePaths;
} catch (e) { } catch (e) {
return null; return null;
} }
} else if (state is NetworkImagePicked) { } else if (state is NetworkImagePicked) {
try { try {
String? url = state.successOrFail.fold((path) => path, (r) => null); final url = state.successOrFail.fold((path) => path, (r) => null);
if (url != null) { if (url != null) {
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
final newPath = final newPath = p.join(directory, _networkImageName(url));
"$directory/IMG_$_timeStampString.${_getExtention(url)}";
final imageFile = File(newPath); final imageFile = File(newPath);
await imageFile.create(); await imageFile.create();
await imageFile.writeAsBytes(response.bodyBytes); await imageFile.writeAsBytes(response.bodyBytes);
imagePaths.add(imageFile.absolute.path); imagePaths.add(imageFile.absolute.path);
await prefs.setStringList(kLocalImagesKey, imagePaths);
return imagePaths;
} else { } else {
return null; return null;
} }
@ -115,59 +111,71 @@ class CoverImagePickerBloc
return null; return null;
} }
} }
await prefs.setStringList(kLocalImagesKey, imagePaths);
return imagePaths;
} }
_pickImages() async { Future<String?> _pickImages() async {
FilePickerResult? result = await getIt<FilePickerService>().pickFiles( final result = await getIt<FilePickerService>().pickFiles(
dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(), dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(),
allowMultiple: false, allowMultiple: false,
type: fp.FileType.image, type: fp.FileType.image,
allowedExtensions: ['jpg', 'png', 'jpeg'], allowedExtensions: allowedExtensions,
); );
if (result != null && result.files.isNotEmpty) { if (result != null && result.files.isNotEmpty) {
final path = result.files.first.path; return result.files.first.path;
if (path != null) {
return path;
} else {
return null;
}
} }
return null; return null;
} }
Future<String> _coverPath() async { Future<String> _coverPath() async {
final directory = await getIt<SettingsLocationCubit>().fetchLocation(); final directory = await getIt<SettingsLocationCubit>().fetchLocation();
return Directory(path.join(directory, 'covers')) return Directory(p.join(directory, 'covers'))
.create(recursive: true) .create(recursive: true)
.then((value) => value.path); .then((value) => value.path);
} }
String get _timeStampString => String _networkImageName(String url) {
DateTime.now().millisecondsSinceEpoch.toString(); return 'IMG_${DateTime.now().millisecondsSinceEpoch.toString()}.${_getExtention(
url,
fromNetwork: true,
)}';
}
String? _getExtention(String path) => path.contains(".jpg") String? _getExtention(
? "jpg" String path, {
: path.contains(".png") bool fromNetwork = false,
? "png" }) {
: path.contains(".jpeg") String? ext;
? "jpeg" if (!fromNetwork) {
: (path.contains("auto=format") && path.contains("unsplash")) final extension = p.extension(path);
? "jpeg" if (extension.isEmpty) {
: null; return null;
_validateUrl(String path) async {
if (_getExtention(path) != null) {
try {
final response = await http.get(Uri.parse(path));
if (response.statusCode == 200) {
return true;
} else {
return false;
}
} catch (e) {
return false;
} }
ext = extension.substring(1);
} else { } else {
final uri = Uri.parse(path);
final paramters = uri.queryParameters;
final dl = paramters['dl'];
if (dl != null) {
ext = p.extension(dl).substring(1);
}
}
if (allowedExtensions.contains(ext)) {
return ext;
}
return null;
}
Future<bool> _validateURL(String path) async {
final extension = _getExtention(path, fromNetwork: true);
if (extension == null) {
return false;
}
try {
final response = await http.head(Uri.parse(path));
return response.statusCode == 200;
} catch (e) {
return false; return false;
} }
} }

View File

@ -82,6 +82,7 @@ class ShareActionList extends StatelessWidget {
final docShareBloc = context.read<DocShareBloc>(); final docShareBloc = context.read<DocShareBloc>();
return PopoverActionList<ShareActionWrapper>( return PopoverActionList<ShareActionWrapper>(
direction: PopoverDirection.bottomWithCenterAligned, direction: PopoverDirection.bottomWithCenterAligned,
offset: const Offset(0, 8),
actions: ShareAction.values actions: ShareAction.values
.map((action) => ShareActionWrapper(action)) .map((action) => ShareActionWrapper(action))
.toList(), .toList(),

View File

@ -83,11 +83,10 @@ class UserWorkspaceListener {
PublishNotifier(); PublishNotifier();
FolderNotificationListener? _listener; FolderNotificationListener? _listener;
final UserProfilePB _userProfile;
UserWorkspaceListener({ UserWorkspaceListener({
required UserProfilePB userProfile, required UserProfilePB userProfile,
}) : _userProfile = userProfile; });
void start({ void start({
void Function(AuthNotifyValue)? onAuthChanged, void Function(AuthNotifyValue)? onAuthChanged,
@ -106,14 +105,18 @@ class UserWorkspaceListener {
_settingChangedNotifier?.addPublishListener(onSettingUpdated); _settingChangedNotifier?.addPublishListener(onSettingUpdated);
} }
// The "current-workspace" is predefined in the backend. Do not try to
// modify it
_listener = FolderNotificationListener( _listener = FolderNotificationListener(
objectId: _userProfile.token, objectId: "current-workspace",
handler: _handleObservableType, handler: _handleObservableType,
); );
} }
void _handleObservableType( void _handleObservableType(
FolderNotification ty, Either<Uint8List, FlowyError> result) { FolderNotification ty,
Either<Uint8List, FlowyError> result,
) {
switch (ty) { switch (ty) {
case FolderNotification.DidCreateWorkspace: case FolderNotification.DidCreateWorkspace:
case FolderNotification.DidDeleteWorkspace: case FolderNotification.DidDeleteWorkspace:

View File

@ -48,6 +48,7 @@ class AddButton extends StatelessWidget {
return PopoverActionList<PopoverAction>( return PopoverActionList<PopoverAction>(
direction: PopoverDirection.bottomWithLeftAligned, direction: PopoverDirection.bottomWithLeftAligned,
actions: actions, actions: actions,
offset: const Offset(0, 8),
buildChild: (controller) { buildChild: (controller) {
return FlowyIconButton( return FlowyIconButton(
width: 22, width: 22,

View File

@ -42,6 +42,7 @@ class BubbleActionList extends StatelessWidget {
return PopoverActionList<PopoverAction>( return PopoverActionList<PopoverAction>(
direction: PopoverDirection.topWithRightAligned, direction: PopoverDirection.topWithRightAligned,
actions: actions, actions: actions,
offset: const Offset(0, -8),
buildChild: (controller) { buildChild: (controller) {
return FlowyTextButton( return FlowyTextButton(
'?', '?',

View File

@ -13,6 +13,7 @@ class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
final Widget Function(PopoverController) buildChild; final Widget Function(PopoverController) buildChild;
final VoidCallback? onClosed; final VoidCallback? onClosed;
final bool asBarrier; final bool asBarrier;
final Offset offset;
const PopoverActionList({ const PopoverActionList({
required this.actions, required this.actions,
@ -22,6 +23,7 @@ class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
this.onClosed, this.onClosed,
this.direction = PopoverDirection.rightWithTopAligned, this.direction = PopoverDirection.rightWithTopAligned,
this.asBarrier = false, this.asBarrier = false,
this.offset = Offset.zero,
this.constraints = const BoxConstraints( this.constraints = const BoxConstraints(
minWidth: 120, minWidth: 120,
maxWidth: 460, maxWidth: 460,
@ -54,6 +56,7 @@ class _PopoverActionListState<T extends PopoverAction>
constraints: widget.constraints, constraints: widget.constraints,
direction: widget.direction, direction: widget.direction,
mutex: widget.mutex, mutex: widget.mutex,
offset: widget.offset,
triggerActions: PopoverTriggerFlags.none, triggerActions: PopoverTriggerFlags.none,
onClose: widget.onClosed, onClose: widget.onClosed,
popupBuilder: (BuildContext popoverContext) { popupBuilder: (BuildContext popoverContext) {

View File

@ -69,6 +69,37 @@ async function moveKanbanBoardRow() {
// Create row in no status group // Create row in no status group
const firstGroup = databaseController.groups.getValue()[1]; const firstGroup = databaseController.groups.getValue()[1];
const secondGroup = databaseController.groups.getValue()[2]; const secondGroup = databaseController.groups.getValue()[2];
// subscribe the group changes
firstGroup.subscribe({
onRemoveRow: (groupId, deleteRowId) => {
console.log(groupId + 'did remove:' + deleteRowId);
},
onInsertRow: (groupId, rowPB) => {
console.log(groupId + 'did insert:' + rowPB.id);
},
onUpdateRow: (groupId, rowPB) => {
console.log(groupId + 'did update:' + rowPB.id);
},
onCreateRow: (groupId, rowPB) => {
console.log(groupId + 'did create:' + rowPB.id);
},
});
secondGroup.subscribe({
onRemoveRow: (groupId, deleteRowId) => {
console.log(groupId + 'did remove:' + deleteRowId);
},
onInsertRow: (groupId, rowPB) => {
console.log(groupId + 'did insert:' + rowPB.id);
},
onUpdateRow: (groupId, rowPB) => {
console.log(groupId + 'did update:' + rowPB.id);
},
onCreateRow: (groupId, rowPB) => {
console.log(groupId + 'did create:' + rowPB.id);
},
});
const row = firstGroup.rowAtIndex(0).unwrap(); const row = firstGroup.rowAtIndex(0).unwrap();
await databaseController.moveRow(row.id, secondGroup.groupId); await databaseController.moveRow(row.id, secondGroup.groupId);

View File

@ -61,12 +61,19 @@ export class DatabaseBackendService {
return DatabaseEventCreateRow(payload); return DatabaseEventCreateRow(payload);
}; };
/// Move a row to another group /// Move the row from one group to another group
moveRow = (rowId: string, groupId?: string) => { /// [groupId] can be the moving row's group id or others.
const payload = MoveGroupRowPayloadPB.fromObject({ view_id: this.viewId, from_row_id: rowId }); /// [toRowId] is used to locate the moving row location.
if (groupId !== undefined) { moveGroupRow = (fromRowId: string, groupId: string, toRowId?: string) => {
payload.to_group_id = groupId; const payload = MoveGroupRowPayloadPB.fromObject({
view_id: this.viewId,
from_row_id: fromRowId,
to_group_id: groupId,
});
if (toRowId !== undefined) {
payload.to_row_id = toRowId;
} }
return DatabaseEventMoveGroupRow(payload); return DatabaseEventMoveGroupRow(payload);
}; };
@ -106,6 +113,7 @@ export class DatabaseBackendService {
}; };
/// Get all groups in database /// Get all groups in database
/// It should only call once after the board open
loadGroups = () => { loadGroups = () => {
const payload = DatabaseViewIdPB.fromObject({ value: this.viewId }); const payload = DatabaseViewIdPB.fromObject({ value: this.viewId });
return DatabaseEventGetGroups(payload); return DatabaseEventGetGroups(payload);

View File

@ -76,7 +76,7 @@ export class DatabaseController {
}; };
moveRow = (rowId: string, groupId: string) => { moveRow = (rowId: string, groupId: string) => {
return this.backendService.moveRow(rowId, groupId); return this.backendService.moveGroupRow(rowId, groupId);
}; };
exchangeRow = async (fromRowId: string, toRowId: string) => { exchangeRow = async (fromRowId: string, toRowId: string) => {

View File

@ -115,6 +115,8 @@ pub fn move_group_row(
} }
// Update the corresponding row's cell content. // Update the corresponding row's cell content.
// If the from_index is none which means the row is not belong to this group before and
// it is moved from other groups.
if from_index.is_none() { if from_index.is_none() {
let cell_rev = make_inserted_cell_rev(&group.id, field_rev); let cell_rev = make_inserted_cell_rev(&group.id, field_rev);
if let Some(cell_rev) = cell_rev { if let Some(cell_rev) = cell_rev {
@ -126,7 +128,6 @@ pub fn move_group_row(
row_changeset row_changeset
.cell_by_field_id .cell_by_field_id
.insert(field_rev.id.clone(), cell_rev); .insert(field_rev.id.clone(), cell_rev);
changeset.updated_rows.push(RowPB::from(*row_rev));
} }
} }
} }

View File

@ -11,6 +11,7 @@ use crate::{
}; };
use flowy_sqlite::kv::KV; use flowy_sqlite::kv::KV;
use folder_model::{AppRevision, WorkspaceRevision}; use folder_model::{AppRevision, WorkspaceRevision};
use lib_dispatch::prelude::ToBytes;
use std::sync::Arc; use std::sync::Arc;
pub struct WorkspaceController { pub struct WorkspaceController {
@ -41,7 +42,6 @@ impl WorkspaceController {
) -> Result<WorkspaceRevision, FlowyError> { ) -> Result<WorkspaceRevision, FlowyError> {
let workspace = self.create_workspace_on_server(params.clone()).await?; let workspace = self.create_workspace_on_server(params.clone()).await?;
let user_id = self.user.user_id()?; let user_id = self.user.user_id()?;
let token = self.user.token()?;
let workspaces = self let workspaces = self
.persistence .persistence
.begin_transaction(|transaction| { .begin_transaction(|transaction| {
@ -53,9 +53,7 @@ impl WorkspaceController {
.map(|workspace_rev| workspace_rev.into()) .map(|workspace_rev| workspace_rev.into())
.collect(); .collect();
let repeated_workspace = RepeatedWorkspacePB { items: workspaces }; let repeated_workspace = RepeatedWorkspacePB { items: workspaces };
send_notification(&token, FolderNotification::DidCreateWorkspace) send_workspace_notification(FolderNotification::DidCreateWorkspace, repeated_workspace);
.payload(repeated_workspace)
.send();
set_current_workspace(&user_id, &workspace.id); set_current_workspace(&user_id, &workspace.id);
Ok(workspace) Ok(workspace)
} }
@ -76,9 +74,7 @@ impl WorkspaceController {
}) })
.await?; .await?;
send_notification(&workspace_id, FolderNotification::DidUpdateWorkspace) send_workspace_notification(FolderNotification::DidUpdateWorkspace, workspace);
.payload(workspace)
.send();
self.update_workspace_on_server(params)?; self.update_workspace_on_server(params)?;
Ok(()) Ok(())
@ -87,7 +83,6 @@ impl WorkspaceController {
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) async fn delete_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> { pub(crate) async fn delete_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> {
let user_id = self.user.user_id()?; let user_id = self.user.user_id()?;
let token = self.user.token()?;
let repeated_workspace = self let repeated_workspace = self
.persistence .persistence
.begin_transaction(|transaction| { .begin_transaction(|transaction| {
@ -95,9 +90,8 @@ impl WorkspaceController {
self.read_workspaces(None, &user_id, &transaction) self.read_workspaces(None, &user_id, &transaction)
}) })
.await?; .await?;
send_notification(&token, FolderNotification::DidDeleteWorkspace)
.payload(repeated_workspace) send_workspace_notification(FolderNotification::DidDeleteWorkspace, repeated_workspace);
.send();
self.delete_workspace_on_server(workspace_id)?; self.delete_workspace_on_server(workspace_id)?;
Ok(()) Ok(())
} }
@ -224,7 +218,6 @@ pub async fn notify_workspace_setting_did_change(
view_id: &str, view_id: &str,
) -> FlowyResult<()> { ) -> FlowyResult<()> {
let user_id = folder_manager.user.user_id()?; let user_id = folder_manager.user.user_id()?;
let token = folder_manager.user.token()?;
let workspace_id = get_current_workspace(&user_id)?; let workspace_id = get_current_workspace(&user_id)?;
let workspace_setting = folder_manager let workspace_setting = folder_manager
@ -250,13 +243,22 @@ pub async fn notify_workspace_setting_did_change(
Ok(setting) Ok(setting)
}) })
.await?; .await?;
send_workspace_notification(
send_notification(&token, FolderNotification::DidUpdateWorkspaceSetting) FolderNotification::DidUpdateWorkspaceSetting,
.payload(workspace_setting) workspace_setting,
.send(); );
Ok(()) Ok(())
} }
/// The [CURRENT_WORKSPACE] represents as the current workspace that opened by the
/// user. Only one workspace can be opened at a time.
const CURRENT_WORKSPACE: &str = "current-workspace";
fn send_workspace_notification<T: ToBytes>(ty: FolderNotification, payload: T) {
send_notification(CURRENT_WORKSPACE, ty)
.payload(payload)
.send();
}
const CURRENT_WORKSPACE_ID: &str = "current_workspace_id"; const CURRENT_WORKSPACE_ID: &str = "current_workspace_id";
pub fn set_current_workspace(_user_id: &str, workspace_id: &str) { pub fn set_current_workspace(_user_id: &str, workspace_id: &str) {

View File

@ -6,39 +6,56 @@ RED="\e[31m"
ENDCOLOR="\e[0m" ENDCOLOR="\e[0m"
printMessage() { printMessage() {
printf "${YELLOW}AppFlowy : $1${ENDCOLOR}\n" printf "${YELLOW}AppFlowy : $1${ENDCOLOR}\n"
} }
printSuccess() { printSuccess() {
printf "${GREEN}AppFlowy : $1${ENDCOLOR}\n" printf "${GREEN}AppFlowy : $1${ENDCOLOR}\n"
} }
printError() { printError() {
printf "${RED}AppFlowy : $1${ENDCOLOR}\n" printf "${RED}AppFlowy : $1${ENDCOLOR}\n"
} }
# Note: This script does not install applications which are installed by the package manager. There are too many package managers out there. # Note: This script does not install applications which are installed by the package manager. There are too many package managers out there.
# Install Rust # Install Rust
printMessage "The Rust programming language is required to compile AppFlowy." printMessage "The Rust programming language is required to compile AppFlowy."
printMessage "We can install it now if you don't already have it on your system." printMessage "We can install it now if you don't already have it on your system."
read -p "$(printSuccess "Do you want to install Rust? [y/N]") " installrust read -p "$(printSuccess "Do you want to install Rust? [y/N]") " installrust
if [ ${installrust^^} == "Y" ]; then if [ ${installrust^^} == "Y" ]; then
printMessage "Installing Rust." printMessage "Installing Rust."
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env source $HOME/.cargo/env
rustup toolchain install stable rustup toolchain install stable
rustup default stable rustup default stable
else else
printMessage "Skipping Rust installation." printMessage "Skipping Rust installation."
fi fi
# Enable the flutter stable channel
printMessage "Setting up Flutter" printMessage "Setting up Flutter"
flutter channel stable # Get the current Flutter version
FLUTTER_VERSION=$(flutter --version | grep -oP 'Flutter \K\S+')
# Check if the current version is 3.3.10
if [ "$FLUTTER_VERSION" = "3.3.10" ]; then
echo "Flutter version is already 3.3.10"
else
# Get the path to the Flutter SDK
FLUTTER_PATH=$(which flutter)
FLUTTER_PATH=${FLUTTER_PATH%/bin/flutter}
current_dir=$(pwd)
cd $FLUTTER_PATH
# Use git to checkout version 3.3.10 of Flutter
git checkout 3.3.10
# Get back to current working directory
cd "$current_dir"
echo "Switched to Flutter version 3.3.10"
fi
# Enable linux desktop # Enable linux desktop
flutter config --enable-linux-desktop flutter config --enable-linux-desktop
@ -47,9 +64,9 @@ flutter config --enable-linux-desktop
flutter doctor flutter doctor
printMessage "Installing keybinder-3.0" printMessage "Installing keybinder-3.0"
if command apt-get &> /dev/null; then if command apt-get &>/dev/null; then
sudo apt-get install keybinder-3.0-dev sudo apt-get install keybinder-3.0-dev
elif command dnf &> /dev/null; then elif command dnf &>/dev/null; then
sudo dnf install keybinder3-devel sudo dnf install keybinder3-devel
else else
echo 'Your system is not supported, please install keybinder3 manually.' echo 'Your system is not supported, please install keybinder3 manually.'
@ -59,11 +76,11 @@ fi
printMessage "Setting up githooks." printMessage "Setting up githooks."
git config core.hooksPath .githooks git config core.hooksPath .githooks
# Install go-gitlint # Install go-gitlint
printMessage "Installing go-gitlint." printMessage "Installing go-gitlint."
GOLINT_FILENAME="go-gitlint_1.1.0_linux_x86_64.tar.gz" GOLINT_FILENAME="go-gitlint_1.1.0_linux_x86_64.tar.gz"
wget https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME} wget https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME}
tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint
rm ${GOLINT_FILENAME} rm ${GOLINT_FILENAME}
# Change to the frontend directory # Change to the frontend directory

View File

@ -17,8 +17,7 @@ printError() {
printf "${RED}AppFlowy : $1${ENDCOLOR}\n" printf "${RED}AppFlowy : $1${ENDCOLOR}\n"
} }
# Install Rust
# Install Rust
printMessage "The Rust programming language is required to compile AppFlowy." printMessage "The Rust programming language is required to compile AppFlowy."
printMessage "We can install it now if you don't already have it on your system." printMessage "We can install it now if you don't already have it on your system."
@ -28,7 +27,7 @@ if [[ "${installrust:-N}" == [Yy] ]]; then
printMessage "Installing Rust." printMessage "Installing Rust."
brew install rustup-init brew install rustup-init
rustup-init -y --default-toolchain=stable rustup-init -y --default-toolchain=stable
source "$HOME/.cargo/env" source "$HOME/.cargo/env"
else else
printMessage "Skipping Rust installation." printMessage "Skipping Rust installation."
@ -36,11 +35,30 @@ fi
# Install sqllite # Install sqllite
printMessage "Installing sqlLite3." printMessage "Installing sqlLite3."
brew install sqlite3 brew install sqlite3
# Enable the flutter stable channel
printMessage "Setting up Flutter" printMessage "Setting up Flutter"
flutter channel stable
# Get the current Flutter version
FLUTTER_VERSION=$(flutter --version | grep -oE 'Flutter [^ ]+' | grep -oE '[^ ]+$')
# Check if the current version is 3.3.10
if [ "$FLUTTER_VERSION" = "3.3.10" ]; then
echo "Flutter version is already 3.3.10"
else
# Get the path to the Flutter SDK
FLUTTER_PATH=$(which flutter)
FLUTTER_PATH=${FLUTTER_PATH%/bin/flutter}
current_dir=$(pwd)
cd $FLUTTER_PATH
# Use git to checkout version 3.3.10 of Flutter
git checkout 3.3.10
# Get back to current working directory
cd "$current_dir"
echo "Switched to Flutter version 3.3.10"
fi
# Enable linux desktop # Enable linux desktop
flutter config --enable-macos-desktop flutter config --enable-macos-desktop
@ -52,11 +70,11 @@ flutter doctor
printMessage "Setting up githooks." printMessage "Setting up githooks."
git config core.hooksPath .githooks git config core.hooksPath .githooks
# Install go-gitlint # Install go-gitlint
printMessage "Installing go-gitlint." printMessage "Installing go-gitlint."
GOLINT_FILENAME="go-gitlint_1.1.0_osx_x86_64.tar.gz" GOLINT_FILENAME="go-gitlint_1.1.0_osx_x86_64.tar.gz"
curl -L https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME} --output ${GOLINT_FILENAME} curl -L https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME} --output ${GOLINT_FILENAME}
tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint
rm ${GOLINT_FILENAME} rm ${GOLINT_FILENAME}
# Change to the frontend directory # Change to the frontend directory

View File

@ -17,7 +17,6 @@ printError() {
printf "${RED}AppFlowy : $1${ENDCOLOR}\n" printf "${RED}AppFlowy : $1${ENDCOLOR}\n"
} }
# Note: This script does not install applications which are installed by the package manager. There are too many package managers out there. # Note: This script does not install applications which are installed by the package manager. There are too many package managers out there.
# Install Rust # Install Rust
@ -46,9 +45,27 @@ else
printSuccess "Rust has been detected on your system, so Rust installation has been skipped" printSuccess "Rust has been detected on your system, so Rust installation has been skipped"
fi fi
# Enable the flutter stable channel
printMessage "Setting up Flutter" printMessage "Setting up Flutter"
flutter channel stable # Get the current Flutter version
FLUTTER_VERSION=$(flutter --version | grep -oP 'Flutter \K\S+')
# Check if the current version is 3.3.10
if [ "$FLUTTER_VERSION" = "3.3.10" ]; then
echo "Flutter version is already 3.3.10"
else
# Get the path to the Flutter SDK
FLUTTER_PATH=$(which flutter)
FLUTTER_PATH=${FLUTTER_PATH%/bin/flutter}
current_dir=$(pwd)
cd $FLUTTER_PATH
# Use git to checkout version 3.3.10 of Flutter
git checkout 3.3.10
# Get back to current working directory
cd "$current_dir"
echo "Switched to Flutter version 3.3.10"
fi
# Add pub cache and cargo to PATH # Add pub cache and cargo to PATH
powershell '[Environment]::SetEnvironmentVariable("PATH", $Env:PATH + ";" + $Env:LOCALAPPDATA + "\Pub\Cache\Bin", [EnvironmentVariableTarget]::User)' powershell '[Environment]::SetEnvironmentVariable("PATH", $Env:PATH + ";" + $Env:LOCALAPPDATA + "\Pub\Cache\Bin", [EnvironmentVariableTarget]::User)'
@ -64,14 +81,14 @@ flutter doctor
printMessage "Setting up githooks." printMessage "Setting up githooks."
git config core.hooksPath .githooks git config core.hooksPath .githooks
# Install go-gitlint # Install go-gitlint
printMessage "Installing go-gitlint." printMessage "Installing go-gitlint."
GOLINT_FILENAME="go-gitlint_1.1.0_windows_x86_64.tar.gz" GOLINT_FILENAME="go-gitlint_1.1.0_windows_x86_64.tar.gz"
if curl --proto '=https' --tlsv1.2 -sSfL https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME} -o ${GOLINT_FILENAME}; then if curl --proto '=https' --tlsv1.2 -sSfL https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME} -o ${GOLINT_FILENAME}; then
tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint.exe tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint.exe
rm ${GOLINT_FILENAME} rm ${GOLINT_FILENAME}
else else
printError "Failed to install go-gitlint" printError "Failed to install go-gitlint"
fi fi
# Change to the frontend directory # Change to the frontend directory