Merge remote-tracking branch 'upstream/main' into export_notify

This commit is contained in:
Sean Riley Hawkins 2022-06-09 09:52:29 +02:00
commit 86150ec6a1
107 changed files with 2605 additions and 1669 deletions

View File

@ -18,7 +18,7 @@ jobs:
- os: ubuntu-latest
flutter_profile: development-linux-x86
- os: macos-latest
flutter_profile: development-mac
flutter_profile: development-mac-x86_64
runs-on: ${{ matrix.os }}
steps:

View File

@ -113,7 +113,7 @@ jobs:
working-directory: frontend
run: |
flutter config --enable-macos-desktop
cargo make --env APP_VERSION=${{ github.ref_name }} --profile production-mac-x86 appflowy
cargo make --env APP_VERSION=${{ github.ref_name }} --profile production-mac-x86_64 appflowy
- name: Archive macOS app
working-directory: ${{ env.MACOS_APP_RELEASE_PATH }}

View File

@ -1,5 +1,40 @@
# Release Notes
## Version 0.0.4 - 2022-06-06
- Drag to adjust the width of a column
- Upgrade to Flutter 3.0
- Native support for M1 chip
- Date supports time formats
- New property: URL
- Keyboard shortcuts support for Grid: press Enter to leave the edit mode; control c/v to copy-paste cell values
### Bug Fixes
- Fixed some bugs
## Version 0.0.4 - beta.3 - 2022-05-02
- Drag to reorder app/ view/ field
- Row record open as a page
- Auto resize the height of the row in the grid
- Support more number formats
- Search column options, supporting Single select, Multi-select, and number format
![May-03-2022 10-03-00](https://user-images.githubusercontent.com/86001920/166394640-a8f1f3bc-5f20-4033-93e9-16bc308d7005.gif)
### Bug Fixes & Improvements
- Improved row/cell data cache
- Fixed some bugs
## Version 0.0.4 - beta.2 - 2022-04-11
- Support properties: Text, Number, Date, Checkbox, Select, Multi-select
- Insert / delete rows
- Add / delete / hide columns
- Edit property
![](https://user-images.githubusercontent.com/12026239/162753644-bf2f4e7a-2367-4d48-87e6-35e244e83a5b.png)
## Version 0.0.4 - beta.1 - 2022-04-08
v0.0.4 - beta.1 is pre-release

View File

@ -45,7 +45,15 @@ APP_ENVIRONMENT = "local"
FLUTTER_FLOWY_SDK_PATH="app_flowy/packages/flowy_sdk"
PROTOBUF_DERIVE_CACHE="../shared-lib/flowy-derive/src/derive_cache/derive_cache.rs"
[env.development-mac]
[env.development-mac-arm64]
RUST_LOG = "info"
TARGET_OS = "macos"
RUST_COMPILE_TARGET = "aarch64-apple-darwin"
BUILD_FLAG = "debug"
FLUTTER_OUTPUT_DIR = "Debug"
PRODUCT_EXT = "app"
[env.development-mac-x86_64]
RUST_LOG = "info"
TARGET_OS = "macos"
RUST_COMPILE_TARGET = "x86_64-apple-darwin"
@ -53,21 +61,23 @@ BUILD_FLAG = "debug"
FLUTTER_OUTPUT_DIR = "Debug"
PRODUCT_EXT = "app"
[env.production-mac-aarch64]
[env.production-mac-arm64]
BUILD_FLAG = "release"
TARGET_OS = "macos"
RUST_COMPILE_TARGET = "aarch64-apple-darwin"
FLUTTER_OUTPUT_DIR = "Release"
PRODUCT_EXT = "app"
APP_ENVIRONMENT = "production"
BUILD_ARCHS = "arm64"
[env.production-mac-x86]
[env.production-mac-x86_64]
BUILD_FLAG = "release"
TARGET_OS = "macos"
RUST_COMPILE_TARGET = "x86_64-apple-darwin"
FLUTTER_OUTPUT_DIR = "Release"
PRODUCT_EXT = "app"
APP_ENVIRONMENT = "production"
BUILD_ARCHS = "x86_64"
[env.development-windows-x86]
TARGET_OS = "windows"
@ -138,6 +148,7 @@ script = [
echo PRODUCT_EXT: ${PRODUCT_EXT}
echo APP_ENVIRONMENT: ${APP_ENVIRONMENT}
echo ${platforms}
echo ${BUILD_ARCHS}
'''
]
script_runner = "@shell"

View File

@ -186,7 +186,8 @@
"row": {
"duplicate": "Duplicate",
"delete": "Delete",
"textPlaceholder": "Empty"
"textPlaceholder": "Empty",
"copyProperty": "Copied property to clipboard"
},
"selectOption": {
"create": "Create",
@ -203,6 +204,10 @@
"colorPannelTitle": "Colors",
"pannelTitle": "Select an option or create one",
"searchOption": "Search for an option"
},
"date": {
"timeHintTextInTwelveHour": "12:00 AM",
"timeHintTextInTwentyFourHour": "12:00"
}
}
}

View File

@ -7,11 +7,11 @@
"letsGoButtonText": "Vamos lá",
"title": "Título",
"signUp": {
"buttonText": "Inscreve-se",
"title": "Inscrever-se @:appName",
"buttonText": "Se inscreva",
"title": "Se inscreva no @:appName",
"getStartedText": "Começar",
"emptyPasswordError": "Senha não pode ser em branco.",
"repeatPasswordEmptyError": "Confirmar a senha não pode ser em branco.",
"emptyPasswordError": "Senha não pode estar em branco.",
"repeatPasswordEmptyError": "Confirmar a senha não pode estar em branco.",
"unmatchedPasswordError": "As senhas não conferem.",
"alreadyHaveAnAccount": "Já possui uma conta?",
"emailHint": "Email",
@ -19,14 +19,14 @@
"repeatPasswordHint": "Confirme a senha"
},
"signIn": {
"loginTitle": "Login to @:appName",
"loginTitle": "Entre no @:appName",
"loginButtonText": "Login",
"buttonText": "Entre",
"forgotPassword": "Esqueceu a senha?",
"emailHint": "Email",
"passwordHint": "Senha",
"dontHaveAnAccount": "Não possui uma conta?",
"repeatPasswordEmptyError": "Confirmar a senha não pode ser em branco.",
"repeatPasswordEmptyError": "Confirmar a senha não pode estar em branco.",
"unmatchedPasswordError": "As senhas não conferem."
},
"workspace": {
@ -67,7 +67,7 @@
"whatsNew": "O que há de novo?",
"help": "Ajuda & Suporte",
"debug": {
"name": "Informação de debug",
"name": "Informação de depuração",
"success": "Copiar informação de debug para o clipboard!",
"fail": "Falha em copiar a informação de debug para o clipboard"
}
@ -104,7 +104,7 @@
},
"button": {
"OK": "OK",
"Cancel": "Canelar",
"Cancel": "Cancelar",
"signIn": "Entrar",
"signOut": "Sair",
"complete": "Completar",
@ -144,3 +144,4 @@
}
}

View File

@ -0,0 +1,146 @@
{
"appName": "AppFlowy",
"defaultUsername": "Me",
"welcomeText": "Bem vindo ao @:appName",
"githubStarText": "Star on GitHub",
"subscribeNewsletterText": "Inscreve-te ao Newsletter",
"letsGoButtonText": "Bora",
"title": "Título",
"signUp": {
"buttonText": "Inscreve-te",
"title": "Inscreve-te ao @:appName",
"getStartedText": "Começar",
"emptyPasswordError": "A palavra-passe não pode estar em branco.",
"repeatPasswordEmptyError": "Confirmar a palavra-passe não pode estar em branco.",
"unmatchedPasswordError": "As palavras-passes não coincidem.",
"alreadyHaveAnAccount": "Já possuis uma conta?",
"emailHint": "Email",
"passwordHint": "Password",
"repeatPasswordHint": "Confirma a tua password"
},
"signIn": {
"loginTitle": "Entre no @:appName",
"loginButtonText": "Login",
"buttonText": "Entre",
"forgotPassword": "Esqueceste-te da tua palavra-passe?",
"emailHint": "Email",
"passwordHint": "Palavra-passe",
"dontHaveAnAccount": "Não possuis uma conta?",
"repeatPasswordEmptyError": "Confirmar a palavra-passe não pode estar em branco.",
"unmatchedPasswordError": "As palavras-passes não conferem."
},
"workspace": {
"create": "Cria um ambiente de trabalho",
"hint": "ambiente de trabalho",
"notFoundError": "Ambiente de trabalho não encontrada"
},
"shareAction": {
"buttonText": "Partilhar",
"workInProgress": "Em breve",
"markdown": "Markdown",
"copyLink": "Copiar o link"
},
"disclosureAction": {
"rename": "Renomear",
"delete": "Apagar",
"duplicate": "Duplicar"
},
"blankPageTitle": "Página em branco",
"newPageText": "Nova página",
"trash": {
"text": "Lixo",
"restoreAll": "Restaurar todos",
"deleteAll": "Apagar todos",
"pageHeader": {
"fileName": "Nome do ficheiro",
"lastModified": "Última modificação",
"created": "Criado"
}
},
"deletePagePrompt": {
"text": "Esta página está no lixo",
"restore": "Restaurar a página",
"deletePermanent": "Apagar permanentemente"
},
"dialogCreatePageNameHint": "Nome da página",
"questionBubble": {
"whatsNew": "O que há de novo?",
"help": "Ajuda & Suporte",
"debug": {
"name": "Informação de depuração",
"success": "Copiar informação de depuração para o clipboard!",
"fail": "Falha em copiar a informação de depuração para o clipboard"
}
},
"menuAppHeader": {
"addPageTooltip": "Adiciona uma nova página.",
"defaultNewPageName": "Sem título",
"renameDialog": "Renomear"
},
"toolbar": {
"undo": "Desfazer",
"redo": "Refazer",
"bold": "Negrito",
"italic": "Itálico",
"underline": "Sublinhado",
"strike": "Riscado",
"numList": "Lista numerada",
"bulletList": "Lista com marcadores",
"checkList": "Lista de verificação",
"inlineCode": "Embutir código",
"quote": "Citação em bloco",
"header": "Cabeçalho",
"highlight": "Realçar"
},
"tooltip": {
"lightMode": "Mudar para o modo Claro.",
"darkMode": "Mudar para o modo Escuro."
},
"contactsPage": {
"title": "Conctatos",
"whatsHappening": "O que está a acontecer nesta semana?",
"addContact": "Adicionar um conctato",
"editContact": "Editar um conctato"
},
"button": {
"OK": "OK",
"Cancel": "Cancelar",
"signIn": "Entrar",
"signOut": "Sair",
"complete": "Completar",
"save": "Guardar"
},
"label": {
"welcome": "Bem vindo!",
"firstName": "Nome",
"middleName": "Nome do Meio",
"lastName": "Apelido",
"stepX": "Passo {X}"
},
"oAuth": {
"err": {
"failedTitle": "Erro ao conectar à sua conta.",
"failedMsg": "Verifica se concluiste o processo de login no teu navegador."
},
"google": {
"title": "GOOGLE SIGN-IN",
"instruction1": "Para importar os teus Conctatos do Google, tens de autorizar esta aplicação usando o teu navegador web.",
"instruction2": "Copia este código para a tua área de transferências clicando no ícone ou selecionando o texto:",
"instruction3": "Navega até o link a seguir no seu navegador e digite o código acima:",
"instruction4": "Clica no botão abaixo ao concluir a inscrição:"
}
},
"settings": {
"title": "Definições",
"menu": {
"appearance": "Aparência",
"language": "Idioma",
"open": "Abrir as Definições"
},
"appearance": {
"lightLabel": "Modo Claro",
"darkLabel": "Modo Escuro"
}
}
}

View File

@ -141,6 +141,68 @@
"lightLabel": "Светлая тема",
"darkLabel": "Тёмная тема"
}
},
"grid": {
"settings": {
"filter": "Фильтр",
"sortBy": "Сортировать",
"Properties": "Свойства"
},
"field": {
"hide": "Скрыть",
"insertLeft": "Вставить слева",
"insertRight": "Вставить справа",
"duplicate": "Дублировать",
"delete": "Удалить",
"textFieldName": "Текст",
"checkboxFieldName": "Checkbox",
"dateFieldName": "Дата",
"numberFieldName": "Число",
"singleSelectFieldName": "Выбор",
"multiSelectFieldName": "Выбор многих",
"urlFieldName": "URL",
"numberFormat": " Формат числа",
"dateFormat": " Формат даты",
"includeTime": " Время",
"dateFormatFriendly": "День Месяц, Год",
"dateFormatISO": "Год-Месяц-День",
"dateFormatLocal": "Год/Месяц/День",
"dateFormatUS": "Год/Месяц/День",
"timeFormat": " Форматировать время",
"invalidTimeFormat": "Неверный формат",
"timeFormatTwelveHour": "12 часов",
"timeFormatTwentyFourHour": "24 часа",
"addSelectOption": "Добавить вариант",
"optionTitle": "Варианты",
"addOption": "Добавить",
"editProperty": "Редактировать свойство"
},
"row": {
"duplicate": "Дублировать",
"delete": "Удалить",
"textPlaceholder": "Пусто",
"copyProperty": "Свойство скопировано"
},
"selectOption": {
"create": "Создать",
"purpleColor": "Фиолетовый",
"pinkColor": "Розовый",
"lightPinkColor": "Светло-розовый",
"orangeColor": "Оранжевый",
"yellowColor": "Желтый",
"limeColor": "Ярко-зелёный",
"greenColor": "Зелёный",
"aquaColor": "Морской волны",
"blueColor": "Синий",
"deleteTag": "Удалить вариант",
"colorPannelTitle": "Цвета",
"pannelTitle": "Выберите или создайте вариант",
"searchOption": "Поиск"
},
"date": {
"timeHintTextInTwelveHour": "12:00 AM",
"timeHintTextInTwentyFourHour": "12:00"
}
}
}

View File

@ -16,6 +16,7 @@ import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-user-data-model/user_profile.pb.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get_it/get_it.dart';
class DependencyResolver {
@ -46,6 +47,8 @@ void _resolveUserDeps(GetIt getIt) {
}
void _resolveHomeDeps(GetIt getIt) {
getIt.registerSingleton(FToast());
getIt.registerSingleton(MenuSharedState());
getIt.registerFactoryParam<UserListener, UserProfile, void>(

View File

@ -67,7 +67,8 @@ class ApplicationWidget extends StatelessWidget {
}) : super(key: key);
@override
Widget build(BuildContext context) => ChangeNotifierProvider.value(
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: settingModel,
builder: (context, _) {
const ratio = 1.73;
@ -102,6 +103,7 @@ class ApplicationWidget extends StatelessWidget {
},
);
}
}
class AppGlobals {
static GlobalKey<NavigatorState> rootNavKey = GlobalKey();

View File

@ -12,14 +12,14 @@ class DocumentService {
await FolderEventSetLatestView(ViewId(value: docId)).send();
final payload = TextBlockId(value: docId);
return BlockEventGetBlockData(payload).send();
return TextBlockEventGetBlockData(payload).send();
}
Future<Either<TextBlockDelta, FlowyError>> composeDelta({required String docId, required String data}) {
final payload = TextBlockDelta.create()
..blockId = docId
..deltaStr = data;
return BlockEventApplyDelta(payload).send();
return TextBlockEventApplyDelta(payload).send();
}
Future<Either<Unit, FlowyError>> closeDocument({required String docId}) {

View File

@ -10,7 +10,7 @@ class ShareService {
..viewId = docId
..exportType = type;
return BlockEventExportDocument(request).send();
return TextBlockEventExportDocument(request).send();
}
Future<Either<ExportData, FlowyError>> exportText(String docId) {

View File

@ -2,7 +2,7 @@ part of 'cell_service.dart';
typedef GridCellContext = _GridCellContext<String, String>;
typedef GridSelectOptionCellContext = _GridCellContext<SelectOptionCellData, String>;
typedef GridDateCellContext = _GridCellContext<DateCellData, DateCalData>;
typedef GridDateCellContext = _GridCellContext<DateCellData, CalendarData>;
typedef GridURLCellContext = _GridCellContext<URLCellData, String>;
class GridCellContextBuilder {
@ -31,6 +31,7 @@ class GridCellContextBuilder {
final cellDataLoader = GridCellDataLoader(
gridCell: _gridCell,
parser: DateCellDataParser(),
config: const GridCellDataConfig(reloadOnFieldChanged: true),
);
return GridDateCellContext(
@ -105,7 +106,7 @@ class _GridCellContext<T, D> extends Equatable {
final FieldService _fieldService;
late final CellListener _cellListener;
late final ValueNotifier<T?> _cellDataNotifier;
late final ValueNotifier<T?>? _cellDataNotifier;
bool isListening = false;
VoidCallback? _onFieldChangedFn;
Timer? _loadDataOperation;
@ -163,19 +164,19 @@ class _GridCellContext<T, D> extends Equatable {
}
onCellChangedFn() {
onCellChanged(_cellDataNotifier.value);
onCellChanged(_cellDataNotifier?.value);
if (cellDataLoader.config.reloadOnCellChanged) {
_loadData();
}
}
_cellDataNotifier.addListener(onCellChangedFn);
_cellDataNotifier?.addListener(onCellChangedFn);
return onCellChangedFn;
}
void removeListener(VoidCallback fn) {
_cellDataNotifier.removeListener(fn);
_cellDataNotifier?.removeListener(fn);
}
T? getCellData({bool loadIfNoCache = true}) {
@ -211,13 +212,14 @@ class _GridCellContext<T, D> extends Equatable {
_loadDataOperation?.cancel();
_loadDataOperation = Timer(const Duration(milliseconds: 10), () {
cellDataLoader.loadData().then((data) {
_cellDataNotifier.value = data;
_cellDataNotifier?.value = data;
cellCache.insert(GridCellCacheData(key: _cacheKey, object: data));
});
});
}
void dispose() {
_cellListener.stop();
_loadDataOperation?.cancel();
_saveDataOperation?.cancel();

View File

@ -58,11 +58,7 @@ class GridCellDataLoader<T> extends IGridCellDataLoader<T> {
return fut.then(
(result) => result.fold((Cell cell) {
try {
if (cell.data.isEmpty) {
return null;
} else {
return parser.parserData(cell.data);
}
} catch (e, s) {
Log.error('$parser parser cellData failed, $e');
Log.error('Stack trace \n $s');
@ -102,13 +98,17 @@ class SelectOptionCellDataLoader extends IGridCellDataLoader<SelectOptionCellDat
class StringCellDataParser implements ICellDataParser<String> {
@override
String? parserData(List<int> data) {
return utf8.decode(data);
final s = utf8.decode(data);
return s;
}
}
class DateCellDataParser implements ICellDataParser<DateCellData> {
@override
DateCellData? parserData(List<int> data) {
if (data.isEmpty) {
return null;
}
return DateCellData.fromBuffer(data);
}
}
@ -116,6 +116,9 @@ class DateCellDataParser implements ICellDataParser<DateCellData> {
class SelectOptionCellDataParser implements ICellDataParser<SelectOptionCellData> {
@override
SelectOptionCellData? parserData(List<int> data) {
if (data.isEmpty) {
return null;
}
return SelectOptionCellData.fromBuffer(data);
}
}
@ -123,6 +126,9 @@ class SelectOptionCellDataParser implements ICellDataParser<SelectOptionCellData
class URLCellDataParser implements ICellDataParser<URLCellData> {
@override
URLCellData? parserData(List<int> data) {
if (data.isEmpty) {
return null;
}
return URLCellData.fromBuffer(data);
}
}

View File

@ -31,18 +31,18 @@ class CellDataPersistence implements _GridCellDataPersistence<String> {
}
@freezed
class DateCalData with _$DateCalData {
const factory DateCalData({required DateTime date, String? time}) = _DateCellPersistenceData;
class CalendarData with _$CalendarData {
const factory CalendarData({required DateTime date, String? time}) = _CalendarData;
}
class DateCellDataPersistence implements _GridCellDataPersistence<DateCalData> {
class DateCellDataPersistence implements _GridCellDataPersistence<CalendarData> {
final GridCell gridCell;
DateCellDataPersistence({
required this.gridCell,
});
@override
Future<Option<FlowyError>> save(DateCalData data) {
Future<Option<FlowyError>> save(CalendarData data) {
var payload = DateChangesetPayload.create()..cellIdentifier = _cellIdentifier(gridCell);
final date = (data.date.millisecondsSinceEpoch ~/ 1000).toString();

View File

@ -38,9 +38,9 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
emit(state.copyWith(focusedDay: focusedDay));
},
didReceiveCellUpdate: (DateCellData? cellData) {
final dateData = dateDataFromCellData(cellData);
final time = dateData.foldRight("", (dateData, previous) => dateData.time);
emit(state.copyWith(dateData: dateData, time: time));
final calData = calDataFromCellData(cellData);
final time = calData.foldRight("", (dateData, previous) => dateData.time);
emit(state.copyWith(calData: calData, time: time));
},
setIncludeTime: (includeTime) async {
await _updateTypeOption(emit, includeTime: includeTime);
@ -52,7 +52,12 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
await _updateTypeOption(emit, timeFormat: timeFormat);
},
setTime: (time) async {
if (state.calData.isSome()) {
await _updateDateData(emit, time: time);
}
},
didUpdateCalData: (Option<CalendarData> data, Option<String> timeFormatError) {
emit(state.copyWith(calData: data, timeFormatError: timeFormatError));
},
);
},
@ -60,8 +65,8 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
}
Future<void> _updateDateData(Emitter<DateCalState> emit, {DateTime? date, String? time}) {
final DateCalData newDateData = state.dateData.fold(
() => DateCalData(date: date ?? DateTime.now(), time: time),
final CalendarData newDateData = state.calData.fold(
() => CalendarData(date: date ?? DateTime.now(), time: time),
(dateData) {
var newDateData = dateData;
if (date != null && !isSameDay(newDateData.date, date)) {
@ -78,24 +83,22 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
return _saveDateData(emit, newDateData);
}
Future<void> _saveDateData(Emitter<DateCalState> emit, DateCalData newDateData) async {
if (state.dateData == Some(newDateData)) {
Future<void> _saveDateData(Emitter<DateCalState> emit, CalendarData newCalData) async {
if (state.calData == Some(newCalData)) {
return;
}
cellContext.saveCellData(newDateData, resultCallback: (result) {
updateCalData(Option<CalendarData> calData, Option<String> timeFormatError) {
if (!isClosed) add(DateCalEvent.didUpdateCalData(calData, timeFormatError));
}
cellContext.saveCellData(newCalData, resultCallback: (result) {
result.fold(
() => emit(state.copyWith(
dateData: Some(newDateData),
timeFormatError: none(),
)),
() => updateCalData(Some(newCalData), none()),
(err) {
switch (ErrorCode.valueOf(err.code)!) {
case ErrorCode.InvalidDateTimeFormat:
emit(state.copyWith(
dateData: Some(newDateData),
timeFormatError: Some(timeFormatPrompt(err)),
));
updateCalData(none(), Some(timeFormatPrompt(err)));
break;
default:
Log.error(err);
@ -168,7 +171,7 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
);
result.fold(
(l) => emit(state.copyWith(dateTypeOption: newDateTypeOption)),
(l) => emit(state.copyWith(dateTypeOption: newDateTypeOption, timeHintText: _timeHintText(newDateTypeOption))),
(err) => Log.error(err),
);
}
@ -185,6 +188,8 @@ class DateCalEvent with _$DateCalEvent {
const factory DateCalEvent.setIncludeTime(bool includeTime) = _IncludeTime;
const factory DateCalEvent.setTime(String time) = _Time;
const factory DateCalEvent.didReceiveCellUpdate(DateCellData? data) = _DidReceiveCellUpdate;
const factory DateCalEvent.didUpdateCalData(Option<CalendarData> data, Option<String> timeFormatError) =
_DidUpdateCalData;
}
@freezed
@ -194,36 +199,48 @@ class DateCalState with _$DateCalState {
required CalendarFormat format,
required DateTime focusedDay,
required Option<String> timeFormatError,
required Option<DateCalData> dateData,
required Option<CalendarData> calData,
required String? time,
required String timeHintText,
}) = _DateCalState;
factory DateCalState.initial(
DateTypeOption dateTypeOption,
DateCellData? cellData,
) {
Option<DateCalData> dateData = dateDataFromCellData(cellData);
final time = dateData.foldRight("", (dateData, previous) => dateData.time);
Option<CalendarData> calData = calDataFromCellData(cellData);
final time = calData.foldRight("", (dateData, previous) => dateData.time);
return DateCalState(
dateTypeOption: dateTypeOption,
format: CalendarFormat.month,
focusedDay: DateTime.now(),
time: time,
dateData: dateData,
calData: calData,
timeFormatError: none(),
timeHintText: _timeHintText(dateTypeOption),
);
}
}
Option<DateCalData> dateDataFromCellData(DateCellData? cellData) {
String _timeHintText(DateTypeOption typeOption) {
switch (typeOption.timeFormat) {
case TimeFormat.TwelveHour:
return LocaleKeys.grid_date_timeHintTextInTwelveHour.tr();
case TimeFormat.TwentyFourHour:
return LocaleKeys.grid_date_timeHintTextInTwentyFourHour.tr();
}
return "";
}
Option<CalendarData> calDataFromCellData(DateCellData? cellData) {
String? time = timeFromCellData(cellData);
Option<DateCalData> dateData = none();
Option<CalendarData> calData = none();
if (cellData != null) {
final timestamp = cellData.timestamp * 1000;
final date = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
dateData = Some(DateCalData(date: date, time: time));
calData = Some(CalendarData(date: date, time: time));
}
return dateData;
return calData;
}
$fixnum.Int64 timestampFromDateTime(DateTime dateTime) {

View File

@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'cell_service/cell_service.dart';
import 'package:dartz/dartz.dart';
part 'date_cell_bloc.freezed.dart';
class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
@ -17,11 +16,7 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
event.when(
initial: () => _startListening(),
didReceiveCellUpdate: (DateCellData? cellData) {
if (cellData != null) {
emit(state.copyWith(data: Some(cellData)));
} else {
emit(state.copyWith(data: none()));
}
emit(state.copyWith(data: cellData, dateStr: _dateStrFromCellData(cellData)));
},
didReceiveFieldUpdate: (Field value) => emit(state.copyWith(field: value)),
);
@ -60,21 +55,26 @@ class DateCellEvent with _$DateCellEvent {
@freezed
class DateCellState with _$DateCellState {
const factory DateCellState({
required Option<DateCellData> data,
required DateCellData? data,
required String dateStr,
required Field field,
}) = _DateCellState;
factory DateCellState.initial(GridDateCellContext context) {
final cellData = context.getCellData();
Option<DateCellData> data = none();
if (cellData != null) {
data = Some(cellData);
}
return DateCellState(
field: context.field,
data: data,
data: cellData,
dateStr: _dateStrFromCellData(cellData),
);
}
}
String _dateStrFromCellData(DateCellData? cellData) {
String dateStr = "";
if (cellData != null) {
dateStr = cellData.date + " " + cellData.time;
}
return dateStr;
}

View File

@ -1,6 +1,8 @@
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'package:dartz/dartz.dart';
import 'cell_service/cell_service.dart';
part 'number_cell_bloc.freezed.dart';
@ -14,25 +16,28 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
}) : super(NumberCellState.initial(cellContext)) {
on<NumberCellEvent>(
(event, emit) async {
await event.map(
initial: (_Initial value) async {
event.when(
initial: () {
_startListening();
},
didReceiveCellUpdate: (_DidReceiveCellUpdate value) {
emit(state.copyWith(content: value.cellContent ?? ""));
didReceiveCellUpdate: (content) {
emit(state.copyWith(content: content));
},
updateCell: (_UpdateCell value) async {
await _updateCellValue(value, emit);
updateCell: (text) {
cellContext.saveCellData(text, resultCallback: (result) {
result.fold(
() => null,
(err) {
if (!isClosed) add(NumberCellEvent.didReceiveCellUpdate(right(err)));
},
);
});
},
);
},
);
}
Future<void> _updateCellValue(_UpdateCell value, Emitter<NumberCellState> emit) async {
cellContext.saveCellData(value.text);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
@ -47,7 +52,7 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
_onCellChangedFn = cellContext.startListening(
onCellChanged: ((cellContent) {
if (!isClosed) {
add(NumberCellEvent.didReceiveCellUpdate(cellContent));
add(NumberCellEvent.didReceiveCellUpdate(left(cellContent ?? "")));
}
}),
);
@ -58,17 +63,19 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
class NumberCellEvent with _$NumberCellEvent {
const factory NumberCellEvent.initial() = _Initial;
const factory NumberCellEvent.updateCell(String text) = _UpdateCell;
const factory NumberCellEvent.didReceiveCellUpdate(String? cellContent) = _DidReceiveCellUpdate;
const factory NumberCellEvent.didReceiveCellUpdate(Either<String, FlowyError> cellContent) = _DidReceiveCellUpdate;
}
@freezed
class NumberCellState with _$NumberCellState {
const factory NumberCellState({
required String content,
required Either<String, FlowyError> content,
}) = _NumberCellState;
factory NumberCellState.initial(GridCellContext context) {
final cellContent = context.getCellData() ?? "";
return NumberCellState(content: cellContent);
return NumberCellState(
content: left(cellContent),
);
}
}

View File

@ -24,6 +24,9 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
url: cellData?.url ?? "",
));
},
updateURL: (String url) {
cellContext.saveCellData(url, deduplicate: true);
},
);
},
);
@ -53,6 +56,7 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
@freezed
class URLCellEvent with _$URLCellEvent {
const factory URLCellEvent.initial() = _InitialCell;
const factory URLCellEvent.updateURL(String url) = _UpdateURL;
const factory URLCellEvent.didReceiveCellUpdate(URLCellData? cell) = _DidReceiveCellUpdate;
}

View File

@ -24,13 +24,15 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
_startListening();
},
didReceiveFieldUpdate: (field) {
emit(state.copyWith(field: field));
emit(state.copyWith(field: cellContext.field));
},
updateWidth: (offset) {
final defaultWidth = state.field.width.toDouble();
final width = defaultWidth + offset;
if (width > defaultWidth && width < 300) {
_fieldService.updateField(width: width);
startUpdateWidth: (offset) {
final width = state.width + offset;
emit(state.copyWith(width: width));
},
endUpdateWidth: () {
if (state.width != state.field.width.toDouble()) {
_fieldService.updateField(width: state.width);
}
},
);
@ -61,7 +63,8 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
class FieldCellEvent with _$FieldCellEvent {
const factory FieldCellEvent.initial() = _InitialCell;
const factory FieldCellEvent.didReceiveFieldUpdate(Field field) = _DidReceiveFieldUpdate;
const factory FieldCellEvent.updateWidth(double offset) = _UpdateWidth;
const factory FieldCellEvent.startUpdateWidth(double offset) = _StartUpdateWidth;
const factory FieldCellEvent.endUpdateWidth() = _EndUpdateWidth;
}
@freezed
@ -69,10 +72,12 @@ class FieldCellState with _$FieldCellState {
const factory FieldCellState({
required String gridId,
required Field field,
required double width,
}) = _FieldCellState;
factory FieldCellState.initial(GridFieldCellContext cellContext) => FieldCellState(
gridId: cellContext.gridId,
field: cellContext.field,
width: cellContext.field.width.toDouble(),
);
}

View File

@ -1,4 +1,5 @@
import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/format.pbenum.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

View File

@ -1,4 +1,4 @@
import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/format.pbenum.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/protobuf.dart';
@ -8,6 +9,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'cell/cell_service/cell_service.dart';
import 'grid_service.dart';
import 'row/row_service.dart';
import 'dart:collection';
part 'grid_bloc.freezed.dart';
@ -33,19 +35,19 @@ class GridBloc extends Bloc<GridEvent, GridState> {
on<GridEvent>(
(event, emit) async {
await event.map(
initial: (InitialGrid value) async {
await event.when(
initial: () async {
_startListening();
await _loadGrid(emit);
},
createRow: (_CreateRow value) {
createRow: () {
_gridService.createRow();
},
didReceiveRowUpdate: (_DidReceiveRowUpdate value) {
emit(state.copyWith(rows: value.rows, listState: value.listState));
didReceiveRowUpdate: (rows, listState) {
emit(state.copyWith(rows: rows, listState: listState));
},
didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) {
emit(state.copyWith(rows: rowCache.clonedRows, fields: value.fields));
didReceiveFieldUpdate: (fields) {
emit(state.copyWith(rows: rowCache.clonedRows, fields: GridFieldEquatable(fields)));
},
);
},
@ -93,7 +95,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
emit(state.copyWith(
grid: Some(grid),
fields: fieldCache.fields,
fields: GridFieldEquatable(fieldCache.fields),
rows: rowCache.clonedRows,
loadingState: GridLoadingState.finish(left(unit)),
));
@ -117,14 +119,14 @@ class GridState with _$GridState {
const factory GridState({
required String gridId,
required Option<Grid> grid,
required List<Field> fields,
required GridFieldEquatable fields,
required List<GridRow> rows,
required GridLoadingState loadingState,
required GridRowChangeReason listState,
}) = _GridState;
factory GridState.initial(String gridId) => GridState(
fields: [],
fields: const GridFieldEquatable([]),
rows: [],
grid: none(),
gridId: gridId,
@ -138,3 +140,19 @@ class GridLoadingState with _$GridLoadingState {
const factory GridLoadingState.loading() = _Loading;
const factory GridLoadingState.finish(Either<Unit, FlowyError> successOrFail) = _Finish;
}
class GridFieldEquatable extends Equatable {
final List<Field> _fields;
const GridFieldEquatable(List<Field> fields) : _fields = fields;
@override
List<Object?> get props {
return [
_fields.length,
_fields.map((field) => field.width).reduce((value, element) => value + element),
];
}
UnmodifiableListView<Field> get value => UnmodifiableListView(_fields);
}

View File

@ -30,7 +30,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
_rowService.createRow();
},
didReceiveCellDatas: (_DidReceiveCellDatas value) async {
final fields = value.gridCellMap.values.map((e) => CellSnapshot(e.field)).toList();
final fields = value.gridCellMap.values.map((e) => GridCellEquatable(e.field)).toList();
final snapshots = UnmodifiableListView(fields);
emit(state.copyWith(
gridCellMap: value.gridCellMap,
@ -74,26 +74,27 @@ class RowState with _$RowState {
const factory RowState({
required GridRow rowData,
required GridCellMap gridCellMap,
required UnmodifiableListView<CellSnapshot> snapshots,
required UnmodifiableListView<GridCellEquatable> snapshots,
GridRowChangeReason? changeReason,
}) = _RowState;
factory RowState.initial(GridRow rowData, GridCellMap cellDataMap) => RowState(
rowData: rowData,
gridCellMap: cellDataMap,
snapshots: UnmodifiableListView(cellDataMap.values.map((e) => CellSnapshot(e.field)).toList()),
snapshots: UnmodifiableListView(cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList()),
);
}
class CellSnapshot extends Equatable {
class GridCellEquatable extends Equatable {
final Field _field;
const CellSnapshot(Field field) : _field = field;
const GridCellEquatable(Field field) : _field = field;
@override
List<Object?> get props => [
_field.id,
_field.fieldType,
_field.visibility,
_field.width,
];
}

View File

@ -18,7 +18,6 @@ import 'home_stack.dart';
import 'menu/menu.dart';
class HomeScreen extends StatefulWidget {
static GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
final UserProfile user;
final CurrentWorkspaceSetting workspaceSetting;
const HomeScreen(this.user, this.workspaceSetting, {Key? key}) : super(key: key);
@ -52,7 +51,6 @@ class _HomeScreenState extends State<HomeScreen> {
),
],
child: Scaffold(
key: HomeScreen.scaffoldKey,
body: BlocListener<HomeBloc, HomeState>(
listenWhen: (p, c) => p.unauthorized != c.unauthorized,
listener: (context, state) {

View File

@ -2,15 +2,13 @@ import 'dart:io' show Platform;
import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/workspace/application/home/home_bloc.dart';
import 'package:app_flowy/workspace/presentation/home/home_screen.dart';
import 'package:app_flowy/workspace/presentation/home/toast.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:time/time.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:app_flowy/plugin/plugin.dart';
import 'package:app_flowy/workspace/presentation/plugins/blank/blank.dart';
import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
@ -22,11 +20,7 @@ import 'package:flowy_infra/notifier.dart';
typedef NavigationCallback = void Function(String id);
late FToast fToast;
class HomeStack extends StatelessWidget {
static GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
// final Size size;
const HomeStack({Key? key}) : super(key: key);
@override
@ -74,8 +68,7 @@ class _FadingIndexedStackState extends State<FadingIndexedStack> {
@override
void initState() {
super.initState();
fToast = FToast();
fToast.init(HomeScreen.scaffoldKey.currentState!.context);
initToastWithContext(context);
}
@override

View File

@ -28,7 +28,7 @@ class AddButton extends StatelessWidget {
onSelected: onSelected,
).show(context);
},
icon: svgWidget("home/add").padding(horizontal: 3, vertical: 3),
icon: svgWidget("home/add", color: theme.iconColor).padding(horizontal: 3, vertical: 3),
);
}
}
@ -46,8 +46,8 @@ class ActionList {
return CreateItem(
pluginBuilder: pluginBuilder,
onSelected: (builder) {
FlowyOverlay.of(buildContext).remove(_identifier);
onSelected(builder);
FlowyOverlay.of(buildContext).remove(_identifier);
},
);
},

View File

@ -0,0 +1,37 @@
import 'package:app_flowy/startup/startup.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
class FlowyMessageToast extends StatelessWidget {
final String message;
const FlowyMessageToast({required this.message, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: FlowyText.medium(message, color: Colors.white),
),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(4)),
color: Colors.black,
),
);
}
}
void initToastWithContext(BuildContext context) {
getIt<FToast>().init(context);
}
void showMessageToast(String message) {
final child = FlowyMessageToast(message: message);
getIt<FToast>().showToast(
child: child,
gravity: ToastGravity.BOTTOM,
toastDuration: const Duration(seconds: 3),
);
}

View File

@ -29,9 +29,9 @@ class ToolbarIconButton extends StatelessWidget {
iconPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
onPressed: onPressed,
width: width,
icon: isToggled == true ? svgWidget(iconName, color: Colors.white) : svgWidget(iconName),
icon: isToggled == true ? svgWidget(iconName, color: Colors.white) : svgWidget(iconName, color: theme.iconColor),
fillColor: isToggled == true ? theme.main1 : theme.shader6,
hoverColor: isToggled == true ? theme.main1 : theme.shader5,
hoverColor: isToggled == true ? theme.main1 : theme.hover,
tooltipText: tooltipText,
);
}

View File

@ -15,6 +15,7 @@ import 'layout/sizes.dart';
import 'widgets/row/grid_row.dart';
import 'widgets/footer/grid_footer.dart';
import 'widgets/header/grid_header.dart';
import 'widgets/shortcuts.dart';
import 'widgets/toolbar/grid_toolbar.dart';
class GridPage extends StatefulWidget {
@ -40,7 +41,7 @@ class _GridPageState extends State<GridPage> {
return state.loadingState.map(
loading: (_) => const Center(child: CircularProgressIndicator.adaptive()),
finish: (result) => result.successOrFail.fold(
(_) => const FlowyGrid(),
(_) => const GridShortcuts(child: FlowyGrid()),
(err) => FlowyErrorPage(err.toString()),
),
);
@ -91,9 +92,9 @@ class _FlowyGridState extends State<FlowyGrid> {
@override
Widget build(BuildContext context) {
return BlocBuilder<GridBloc, GridState>(
buildWhen: (previous, current) => previous.fields.length != current.fields.length,
buildWhen: (previous, current) => previous.fields != current.fields,
builder: (context, state) {
final contentWidth = GridLayout.headerWidth(state.fields);
final contentWidth = GridLayout.headerWidth(state.fields.value);
final child = _wrapScrollView(
contentWidth,
[

View File

@ -0,0 +1,198 @@
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/widgets.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flowy_infra/size.dart';
import 'package:styled_widget/styled_widget.dart';
class GridCellAccessoryBuildContext {
final BuildContext anchorContext;
final bool isCellEditing;
GridCellAccessoryBuildContext({
required this.anchorContext,
required this.isCellEditing,
});
}
abstract class GridCellAccessory implements Widget {
void onTap();
// The accessory will be hidden if enable() return false;
bool enable() => true;
}
class PrimaryCellAccessory extends StatelessWidget with GridCellAccessory {
final VoidCallback onTapCallback;
final bool isCellEditing;
const PrimaryCellAccessory({
required this.onTapCallback,
required this.isCellEditing,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (isCellEditing) {
return const SizedBox();
} else {
final theme = context.watch<AppTheme>();
return svgWidget("grid/expander", color: theme.main1);
}
}
@override
void onTap() => onTapCallback();
@override
bool enable() => !isCellEditing;
}
typedef AccessoryBuilder = List<GridCellAccessory> Function(GridCellAccessoryBuildContext buildContext);
abstract class CellAccessory extends Widget {
const CellAccessory({Key? key}) : super(key: key);
// The hover will show if the isHover's value is true
ValueNotifier<bool>? get onAccessoryHover;
AccessoryBuilder? get accessoryBuilder;
}
class AccessoryHover extends StatefulWidget {
final CellAccessory child;
final EdgeInsets contentPadding;
const AccessoryHover({
required this.child,
this.contentPadding = EdgeInsets.zero,
Key? key,
}) : super(key: key);
@override
State<AccessoryHover> createState() => _AccessoryHoverState();
}
class _AccessoryHoverState extends State<AccessoryHover> {
late AccessoryHoverState _hoverState;
VoidCallback? _listenerFn;
@override
void initState() {
_hoverState = AccessoryHoverState();
_listenerFn = () => _hoverState.onHover = widget.child.onAccessoryHover?.value ?? false;
widget.child.onAccessoryHover?.addListener(_listenerFn!);
super.initState();
}
@override
void dispose() {
_hoverState.dispose();
if (_listenerFn != null) {
widget.child.onAccessoryHover?.removeListener(_listenerFn!);
_listenerFn = null;
}
super.dispose();
}
@override
Widget build(BuildContext context) {
List<Widget> children = [
const _Background(),
Padding(padding: widget.contentPadding, child: widget.child),
];
final accessoryBuilder = widget.child.accessoryBuilder;
if (accessoryBuilder != null) {
final accessories = accessoryBuilder((GridCellAccessoryBuildContext(
anchorContext: context,
isCellEditing: false,
)));
children.add(
Padding(
padding: const EdgeInsets.only(right: 6),
child: CellAccessoryContainer(accessories: accessories),
).positioned(right: 0),
);
}
return ChangeNotifierProvider.value(
value: _hoverState,
child: MouseRegion(
cursor: SystemMouseCursors.click,
opaque: false,
onEnter: (p) => setState(() => _hoverState.onHover = true),
onExit: (p) => setState(() => _hoverState.onHover = false),
child: Stack(
fit: StackFit.loose,
alignment: AlignmentDirectional.center,
children: children,
),
),
);
}
}
class AccessoryHoverState extends ChangeNotifier {
bool _onHover = false;
set onHover(bool value) {
if (_onHover != value) {
_onHover = value;
notifyListeners();
}
}
bool get onHover => _onHover;
}
class _Background extends StatelessWidget {
const _Background({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return Consumer<AccessoryHoverState>(
builder: (context, state, child) {
if (state.onHover) {
return FlowyHoverContainer(
style: HoverStyle(borderRadius: Corners.s6Border, hoverColor: theme.shader6),
);
} else {
return const SizedBox();
}
},
);
}
}
class CellAccessoryContainer extends StatelessWidget {
final List<GridCellAccessory> accessories;
const CellAccessoryContainer({required this.accessories, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
final children = accessories.where((accessory) => accessory.enable()).map((accessory) {
final hover = FlowyHover(
style: HoverStyle(hoverColor: theme.bg3, backgroundColor: theme.surface),
builder: (_, onHover) => Container(
width: 26,
height: 26,
padding: const EdgeInsets.all(3),
child: accessory,
),
);
return GestureDetector(
child: hover,
behavior: HitTestBehavior.opaque,
onTap: () => accessory.onTap(),
);
}).toList();
return Wrap(children: children, spacing: 6);
}
}

View File

@ -1,13 +1,10 @@
import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show FieldType;
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
import 'package:styled_widget/styled_widget.dart';
import 'cell_accessory.dart';
import 'cell_shortcuts.dart';
import 'checkbox_cell.dart';
import 'date_cell/date_cell.dart';
import 'number_cell.dart';
@ -48,24 +45,132 @@ class BlankCell extends StatelessWidget {
}
}
abstract class GridCellWidget implements FlowyHoverWidget {
@override
final ValueNotifier<bool> onFocus = ValueNotifier<bool>(false);
abstract class CellEditable {
GridCellFocusListener get beginFocus;
final GridCellRequestFocusNotifier requestFocus = GridCellRequestFocusNotifier();
ValueNotifier<bool> get onCellFocus;
ValueNotifier<bool> get onCellEditing;
}
class GridCellRequestFocusNotifier extends ChangeNotifier {
VoidCallback? _listener;
abstract class GridCellWidget extends StatefulWidget implements CellAccessory, CellEditable, CellShortcuts {
GridCellWidget({Key? key}) : super(key: key) {
onCellEditing.addListener(() {
onCellFocus.value = onCellEditing.value;
});
}
@override
void addListener(VoidCallback listener) {
final ValueNotifier<bool> onCellFocus = ValueNotifier<bool>(false);
// When the cell is focused, we assume that the accessory alse be hovered.
@override
ValueNotifier<bool> get onAccessoryHover => onCellFocus;
@override
final ValueNotifier<bool> onCellEditing = ValueNotifier<bool>(false);
@override
List<GridCellAccessory> Function(GridCellAccessoryBuildContext buildContext)? get accessoryBuilder => null;
@override
final GridCellFocusListener beginFocus = GridCellFocusListener();
@override
final Map<CellKeyboardKey, CellKeyboardAction> shortcutHandlers = {};
}
abstract class GridCellState<T extends GridCellWidget> extends State<T> {
@override
void initState() {
widget.beginFocus.setListener(() => requestBeginFocus());
widget.shortcutHandlers[CellKeyboardKey.onCopy] = () => onCopy();
widget.shortcutHandlers[CellKeyboardKey.onInsert] = () {
Clipboard.getData("text/plain").then((data) {
final s = data?.text;
if (s is String) {
onInsert(s);
}
});
};
super.initState();
}
@override
void didUpdateWidget(covariant T oldWidget) {
if (oldWidget != this) {
widget.beginFocus.setListener(() => requestBeginFocus());
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
widget.beginFocus.removeAllListener();
super.dispose();
}
void requestBeginFocus();
String? onCopy() => null;
void onInsert(String value) {}
}
abstract class GridFocusNodeCellState<T extends GridCellWidget> extends GridCellState<T> {
SingleListenrFocusNode focusNode = SingleListenrFocusNode();
@override
void initState() {
widget.shortcutHandlers[CellKeyboardKey.onEnter] = () => focusNode.unfocus();
_listenOnFocusNodeChanged();
super.initState();
}
@override
void didUpdateWidget(covariant T oldWidget) {
if (oldWidget != this) {
_listenOnFocusNodeChanged();
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
widget.shortcutHandlers.clear();
focusNode.removeAllListener();
focusNode.dispose();
super.dispose();
}
@override
void requestBeginFocus() {
if (focusNode.hasFocus == false && focusNode.canRequestFocus) {
FocusScope.of(context).requestFocus(focusNode);
}
}
void _listenOnFocusNodeChanged() {
widget.onCellEditing.value = focusNode.hasFocus;
focusNode.setListener(() {
widget.onCellEditing.value = focusNode.hasFocus;
focusChanged();
});
}
Future<void> focusChanged() async {}
}
class GridCellFocusListener extends ChangeNotifier {
VoidCallback? _listener;
void setListener(VoidCallback listener) {
if (_listener != null) {
removeListener(_listener!);
}
_listener = listener;
super.addListener(listener);
addListener(listener);
}
void removeAllListener() {
@ -81,10 +186,10 @@ class GridCellRequestFocusNotifier extends ChangeNotifier {
abstract class GridCellStyle {}
class CellSingleFocusNode extends FocusNode {
class SingleListenrFocusNode extends FocusNode {
VoidCallback? _listener;
void setSingleListener(VoidCallback listener) {
void setListener(VoidCallback listener) {
if (_listener != null) {
removeListener(_listener!);
}
@ -93,120 +198,9 @@ class CellSingleFocusNode extends FocusNode {
super.addListener(listener);
}
void removeSingleListener() {
void removeAllListener() {
if (_listener != null) {
removeListener(_listener!);
}
}
}
class CellStateNotifier extends ChangeNotifier {
bool _isFocus = false;
bool _onEnter = false;
set isFocus(bool value) {
if (_isFocus != value) {
_isFocus = value;
notifyListeners();
}
}
set onEnter(bool value) {
if (_onEnter != value) {
_onEnter = value;
notifyListeners();
}
}
bool get isFocus => _isFocus;
bool get onEnter => _onEnter;
}
class CellContainer extends StatelessWidget {
final GridCellWidget child;
final Widget? expander;
final double width;
final RegionStateNotifier rowStateNotifier;
const CellContainer({
Key? key,
required this.child,
required this.width,
required this.rowStateNotifier,
this.expander,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProxyProvider<RegionStateNotifier, CellStateNotifier>(
create: (_) => CellStateNotifier(),
update: (_, row, cell) => cell!..onEnter = row.onEnter,
child: Selector<CellStateNotifier, bool>(
selector: (context, notifier) => notifier.isFocus,
builder: (context, isFocus, _) {
Widget container = Center(child: child);
child.onFocus.addListener(() {
Provider.of<CellStateNotifier>(context, listen: false).isFocus = child.onFocus.value;
});
if (expander != null) {
container = CellEnterRegion(child: container, expander: expander!);
}
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => child.requestFocus.notify(),
child: Container(
constraints: BoxConstraints(maxWidth: width, minHeight: 46),
decoration: _makeBoxDecoration(context, isFocus),
padding: GridSize.cellContentInsets,
child: container,
),
);
},
),
);
}
BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) {
final theme = context.watch<AppTheme>();
if (isFocus) {
final borderSide = BorderSide(color: theme.main1, width: 1.0);
return BoxDecoration(border: Border.fromBorderSide(borderSide));
} else {
final borderSide = BorderSide(color: theme.shader5, width: 1.0);
return BoxDecoration(border: Border(right: borderSide, bottom: borderSide));
}
}
}
class CellEnterRegion extends StatelessWidget {
final Widget child;
final Widget expander;
const CellEnterRegion({required this.child, required this.expander, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Selector<CellStateNotifier, bool>(
selector: (context, notifier) => notifier.onEnter,
builder: (context, onEnter, _) {
List<Widget> children = [child];
if (onEnter) {
children.add(expander.positioned(right: 0));
}
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = true,
onExit: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = false,
child: Stack(
alignment: AlignmentDirectional.center,
fit: StackFit.expand,
// alignment: AlignmentDirectional.centerEnd,
children: children,
),
);
},
);
}
}

View File

@ -0,0 +1,140 @@
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'cell_accessory.dart';
import 'cell_builder.dart';
import 'cell_shortcuts.dart';
class CellContainer extends StatelessWidget {
final GridCellWidget child;
final AccessoryBuilder? accessoryBuilder;
final double width;
final RegionStateNotifier rowStateNotifier;
const CellContainer({
Key? key,
required this.child,
required this.width,
required this.rowStateNotifier,
this.accessoryBuilder,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProxyProvider<RegionStateNotifier, CellContainerNotifier>(
create: (_) => CellContainerNotifier(child),
update: (_, rowStateNotifier, cellStateNotifier) => cellStateNotifier!..onEnter = rowStateNotifier.onEnter,
child: Selector<CellContainerNotifier, bool>(
selector: (context, notifier) => notifier.isFocus,
builder: (context, isFocus, _) {
Widget container = Center(child: GridCellShortcuts(child: child));
if (accessoryBuilder != null) {
final accessories = accessoryBuilder!(GridCellAccessoryBuildContext(
anchorContext: context,
isCellEditing: isFocus,
));
if (accessories.isNotEmpty) {
container = CellEnterRegion(child: container, accessories: accessories);
}
}
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => child.beginFocus.notify(),
child: Container(
constraints: BoxConstraints(maxWidth: width, minHeight: 46),
decoration: _makeBoxDecoration(context, isFocus),
padding: GridSize.cellContentInsets,
child: container,
),
);
},
),
);
}
BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) {
final theme = context.watch<AppTheme>();
if (isFocus) {
final borderSide = BorderSide(color: theme.main1, width: 1.0);
return BoxDecoration(border: Border.fromBorderSide(borderSide));
} else {
final borderSide = BorderSide(color: theme.shader5, width: 1.0);
return BoxDecoration(border: Border(right: borderSide, bottom: borderSide));
}
}
}
class CellEnterRegion extends StatelessWidget {
final Widget child;
final List<GridCellAccessory> accessories;
const CellEnterRegion({required this.child, required this.accessories, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Selector<CellContainerNotifier, bool>(
selector: (context, notifier) => notifier.onEnter,
builder: (context, onEnter, _) {
List<Widget> children = [child];
if (onEnter) {
children.add(CellAccessoryContainer(accessories: accessories).positioned(right: 0));
}
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (p) => Provider.of<CellContainerNotifier>(context, listen: false).onEnter = true,
onExit: (p) => Provider.of<CellContainerNotifier>(context, listen: false).onEnter = false,
child: Stack(
alignment: AlignmentDirectional.center,
fit: StackFit.expand,
children: children,
),
);
},
);
}
}
class CellContainerNotifier extends ChangeNotifier {
final CellEditable cellEditable;
bool mouted = false;
VoidCallback? _onCellFocusListener;
bool _isFocus = false;
bool _onEnter = false;
CellContainerNotifier(this.cellEditable) {
_onCellFocusListener = () => isFocus = cellEditable.onCellFocus.value;
cellEditable.onCellFocus.addListener(_onCellFocusListener!);
}
@override
void dispose() {
if (_onCellFocusListener != null) {
cellEditable.onCellFocus.removeListener(_onCellFocusListener!);
}
super.dispose();
}
set isFocus(bool value) {
if (_isFocus != value) {
_isFocus = value;
notifyListeners();
}
}
set onEnter(bool value) {
if (_onEnter != value) {
_onEnter = value;
notifyListeners();
}
}
bool get isFocus => _isFocus;
bool get onEnter => _onEnter;
}

View File

@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
typedef CellKeyboardAction = dynamic Function();
enum CellKeyboardKey {
onEnter,
onCopy,
onInsert,
}
abstract class CellShortcuts extends Widget {
const CellShortcuts({Key? key}) : super(key: key);
Map<CellKeyboardKey, CellKeyboardAction> get shortcutHandlers;
}
class GridCellShortcuts extends StatelessWidget {
final CellShortcuts child;
const GridCellShortcuts({required this.child, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.enter): const GridCellEnterIdent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC): const GridCellCopyIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyV): const GridCellInsertIntent(),
},
child: Actions(
actions: {
GridCellEnterIdent: GridCellEnterAction(child: child),
GridCellCopyIntent: GridCellCopyAction(child: child),
GridCellInsertIntent: GridCellInsertAction(child: child),
},
child: child,
),
);
}
}
class GridCellEnterIdent extends Intent {
const GridCellEnterIdent();
}
class GridCellEnterAction extends Action<GridCellEnterIdent> {
final CellShortcuts child;
GridCellEnterAction({required this.child});
@override
void invoke(covariant GridCellEnterIdent intent) {
final callback = child.shortcutHandlers[CellKeyboardKey.onEnter];
if (callback != null) {
callback();
}
}
}
class GridCellCopyIntent extends Intent {
const GridCellCopyIntent();
}
class GridCellCopyAction extends Action<GridCellCopyIntent> {
final CellShortcuts child;
GridCellCopyAction({required this.child});
@override
void invoke(covariant GridCellCopyIntent intent) {
final callback = child.shortcutHandlers[CellKeyboardKey.onCopy];
if (callback == null) {
return;
}
final s = callback();
if (s is String) {
Clipboard.setData(ClipboardData(text: s));
}
}
}
class GridCellInsertIntent extends Intent {
const GridCellInsertIntent();
}
class GridCellInsertAction extends Action<GridCellInsertIntent> {
final CellShortcuts child;
GridCellInsertAction({required this.child});
@override
void invoke(covariant GridCellInsertIntent intent) {
final callback = child.shortcutHandlers[CellKeyboardKey.onInsert];
if (callback != null) {
callback();
}
}
}

View File

@ -6,7 +6,7 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'cell_builder.dart';
class CheckboxCell extends StatefulWidget with GridCellWidget {
class CheckboxCell extends GridCellWidget {
final GridCellContextBuilder cellContextBuilder;
CheckboxCell({
required this.cellContextBuilder,
@ -14,17 +14,16 @@ class CheckboxCell extends StatefulWidget with GridCellWidget {
}) : super(key: key);
@override
State<CheckboxCell> createState() => _CheckboxCellState();
GridCellState<CheckboxCell> createState() => _CheckboxCellState();
}
class _CheckboxCellState extends State<CheckboxCell> {
class _CheckboxCellState extends GridCellState<CheckboxCell> {
late CheckboxCellBloc _cellBloc;
@override
void initState() {
final cellContext = widget.cellContextBuilder.build();
_cellBloc = getIt<CheckboxCellBloc>(param1: cellContext)..add(const CheckboxCellEvent.initial());
_listenCellRequestFocus();
super.initState();
}
@ -49,22 +48,23 @@ class _CheckboxCellState extends State<CheckboxCell> {
);
}
@override
void didUpdateWidget(covariant CheckboxCell oldWidget) {
_listenCellRequestFocus();
super.didUpdateWidget(oldWidget);
}
@override
Future<void> dispose() async {
widget.requestFocus.removeAllListener();
_cellBloc.close();
super.dispose();
}
void _listenCellRequestFocus() {
widget.requestFocus.addListener(() {
@override
void requestBeginFocus() {
_cellBloc.add(const CheckboxCellEvent.select());
});
}
@override
String? onCopy() {
if (_cellBloc.state.isSelected) {
return "Yes";
} else {
return "No";
}
}
}

View File

@ -18,7 +18,7 @@ abstract class GridCellDelegate {
GridCellDelegate get delegate;
}
class DateCell extends StatefulWidget with GridCellWidget {
class DateCell extends GridCellWidget {
final GridCellContextBuilder cellContextBuilder;
late final DateCellStyle? cellStyle;
@ -35,10 +35,10 @@ class DateCell extends StatefulWidget with GridCellWidget {
}
@override
State<DateCell> createState() => _DateCellState();
GridCellState<DateCell> createState() => _DateCellState();
}
class _DateCellState extends State<DateCell> {
class _DateCellState extends GridCellState<DateCell> {
late DateCellBloc _cellBloc;
@override
@ -64,7 +64,7 @@ class _DateCellState extends State<DateCell> {
cursor: SystemMouseCursors.click,
child: Align(
alignment: alignment,
child: FlowyText.medium(state.data.foldRight("", (data, _) => data.date), fontSize: 12),
child: FlowyText.medium(state.dateStr, fontSize: 12),
),
),
),
@ -76,8 +76,8 @@ class _DateCellState extends State<DateCell> {
void _showCalendar(BuildContext context) {
final bloc = context.read<DateCellBloc>();
widget.onFocus.value = true;
final calendar = DateCellEditor(onDismissed: () => widget.onFocus.value = false);
widget.onCellEditing.value = true;
final calendar = DateCellEditor(onDismissed: () => widget.onCellEditing.value = false);
calendar.show(
context,
cellContext: bloc.cellContext.clone(),
@ -89,4 +89,10 @@ class _DateCellState extends State<DateCell> {
_cellBloc.close();
super.dispose();
}
@override
void requestBeginFocus() {}
@override
String? onCopy() => _cellBloc.state.dateStr;
}

View File

@ -160,18 +160,21 @@ class _CellCalendarWidget extends StatelessWidget {
),
),
selectedDayPredicate: (day) {
return state.dateData.fold(
return state.calData.fold(
() => false,
(dateData) => isSameDay(dateData.date, day),
);
},
onDaySelected: (selectedDay, focusedDay) {
_CalDateTimeSetting.hide(context);
context.read<DateCalBloc>().add(DateCalEvent.selectDay(selectedDay));
},
onFormatChanged: (format) {
_CalDateTimeSetting.hide(context);
context.read<DateCalBloc>().add(DateCalEvent.setCalFormat(format));
},
onPageChanged: (focusedDay) {
_CalDateTimeSetting.hide(context);
context.read<DateCalBloc>().add(DateCalEvent.setFocusedDay(focusedDay));
},
);
@ -234,6 +237,7 @@ class _TimeTextFieldState extends State<_TimeTextField> {
if (widget.bloc.state.dateTypeOption.includeTime) {
_focusNode.addListener(() {
if (mounted) {
_CalDateTimeSetting.hide(context);
widget.bloc.add(DateCalEvent.setTime(_controller.text));
}
});
@ -257,6 +261,7 @@ class _TimeTextFieldState extends State<_TimeTextField> {
child: RoundedInputField(
height: 40,
focusNode: _focusNode,
hintText: state.timeHintText,
controller: _controller,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
normalBorderColor: theme.shader4,
@ -326,6 +331,7 @@ class _CalDateTimeSetting extends StatefulWidget {
}
void show(BuildContext context) {
hide(context);
FlowyOverlay.of(context).insertWithAnchor(
widget: OverlayContainer(
child: this,
@ -337,6 +343,10 @@ class _CalDateTimeSetting extends StatefulWidget {
anchorOffset: const Offset(20, 0),
);
}
static void hide(BuildContext context) {
FlowyOverlay.of(context).remove(identifier());
}
}
class _CalDateTimeSettingState extends State<_CalDateTimeSetting> {

View File

@ -1,5 +1,4 @@
import 'dart:async';
import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/workspace/application/grid/prelude.dart';
import 'package:flutter/material.dart';
@ -7,7 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'cell_builder.dart';
class NumberCell extends StatefulWidget with GridCellWidget {
class NumberCell extends GridCellWidget {
final GridCellContextBuilder cellContextBuilder;
NumberCell({
@ -16,41 +15,37 @@ class NumberCell extends StatefulWidget with GridCellWidget {
}) : super(key: key);
@override
State<NumberCell> createState() => _NumberCellState();
GridFocusNodeCellState<NumberCell> createState() => _NumberCellState();
}
class _NumberCellState extends State<NumberCell> {
class _NumberCellState extends GridFocusNodeCellState<NumberCell> {
late NumberCellBloc _cellBloc;
late TextEditingController _controller;
late CellSingleFocusNode _focusNode;
Timer? _delayOperation;
@override
void initState() {
final cellContext = widget.cellContextBuilder.build();
_cellBloc = getIt<NumberCellBloc>(param1: cellContext)..add(const NumberCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.content);
_focusNode = CellSingleFocusNode();
_listenFocusNode();
_controller = TextEditingController(text: contentFromState(_cellBloc.state));
super.initState();
}
@override
Widget build(BuildContext context) {
_listenCellRequestFocus(context);
return BlocProvider.value(
value: _cellBloc,
child: BlocConsumer<NumberCellBloc, NumberCellState>(
listener: (context, state) {
if (_controller.text != state.content) {
_controller.text = state.content;
}
},
builder: (context, state) {
return TextField(
child: MultiBlocListener(
listeners: [
BlocListener<NumberCellBloc, NumberCellState>(
listenWhen: (p, c) => p.content != c.content,
listener: (context, state) => _controller.text = contentFromState(state),
),
],
child: TextField(
controller: _controller,
focusNode: _focusNode,
onEditingComplete: () => _focusNode.unfocus(),
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
maxLines: null,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
decoration: const InputDecoration(
@ -58,59 +53,41 @@ class _NumberCellState extends State<NumberCell> {
border: InputBorder.none,
isDense: true,
),
);
},
),
),
);
}
@override
Future<void> dispose() async {
widget.requestFocus.removeAllListener();
_delayOperation?.cancel();
_cellBloc.close();
_focusNode.removeSingleListener();
_focusNode.dispose();
super.dispose();
}
@override
void didUpdateWidget(covariant NumberCell oldWidget) {
if (oldWidget != widget) {
_listenFocusNode();
}
super.didUpdateWidget(oldWidget);
}
Future<void> focusChanged() async {
if (mounted) {
_delayOperation?.cancel();
_delayOperation = Timer(const Duration(milliseconds: 300), () {
if (_cellBloc.isClosed == false && _controller.text != _cellBloc.state.content) {
final number = num.tryParse(_controller.text);
if (number != null) {
if (_cellBloc.isClosed == false && _controller.text != contentFromState(_cellBloc.state)) {
_cellBloc.add(NumberCellEvent.updateCell(_controller.text));
} else {
_controller.text = "";
}
}
});
}
}
void _listenFocusNode() {
widget.onFocus.value = _focusNode.hasFocus;
_focusNode.setSingleListener(() {
widget.onFocus.value = _focusNode.hasFocus;
focusChanged();
});
String contentFromState(NumberCellState state) {
return state.content.fold((l) => l, (r) => "");
}
void _listenCellRequestFocus(BuildContext context) {
widget.requestFocus.addListener(() {
if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
FocusScope.of(context).requestFocus(_focusNode);
@override
String? onCopy() {
return _cellBloc.state.content.fold((content) => content, (r) => null);
}
});
@override
void onInsert(String value) {
_cellBloc.add(NumberCellEvent.updateCell(value));
}
}

View File

@ -64,9 +64,11 @@ class SelectOptionTag extends StatelessWidget {
final String name;
final Color color;
final bool isSelected;
final VoidCallback? onSelected;
const SelectOptionTag({
required this.name,
required this.color,
this.onSelected,
this.isSelected = false,
Key? key,
}) : super(key: key);
@ -74,12 +76,14 @@ class SelectOptionTag extends StatelessWidget {
factory SelectOptionTag.fromSelectOption({
required BuildContext context,
required SelectOption option,
VoidCallback? onSelected,
bool isSelected = false,
}) {
return SelectOptionTag(
name: option.name,
color: option.color.make(context),
isSelected: isSelected,
onSelected: onSelected,
);
}
@ -92,19 +96,12 @@ class SelectOptionTag extends StatelessWidget {
backgroundColor: color,
labelPadding: const EdgeInsets.symmetric(horizontal: 6),
selected: true,
onSelected: (_) {},
onSelected: (_) {
if (onSelected != null) {
onSelected!();
}
},
);
// return Container(
// decoration: BoxDecoration(
// color: option.color.make(context),
// shape: BoxShape.rectangle,
// borderRadius: BorderRadius.circular(8.0),
// ),
// child: Center(child: FlowyText.medium(option.name, fontSize: 12)),
// margin: const EdgeInsets.symmetric(horizontal: 3.0),
// padding: const EdgeInsets.symmetric(horizontal: 6.0),
// );
}
}
@ -136,7 +133,11 @@ class SelectOptionTagCell extends StatelessWidget {
Flexible(
fit: FlexFit.loose,
flex: 2,
child: SelectOptionTag.fromSelectOption(context: context, option: option),
child: SelectOptionTag.fromSelectOption(
context: context,
option: option,
onSelected: () => onSelected(option),
),
),
const Spacer(),
...children,

View File

@ -20,7 +20,7 @@ class SelectOptionCellStyle extends GridCellStyle {
});
}
class SingleSelectCell extends StatefulWidget with GridCellWidget {
class SingleSelectCell extends GridCellWidget {
final GridCellContextBuilder cellContextBuilder;
late final SelectOptionCellStyle? cellStyle;
@ -59,7 +59,7 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
return _SelectOptionCell(
selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle,
onFocus: (value) => widget.onFocus.value = value,
onFocus: (value) => widget.onCellEditing.value = value,
cellContextBuilder: widget.cellContextBuilder);
},
),
@ -74,7 +74,7 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
}
//----------------------------------------------------------------
class MultiSelectCell extends StatefulWidget with GridCellWidget {
class MultiSelectCell extends GridCellWidget {
final GridCellContextBuilder cellContextBuilder;
late final SelectOptionCellStyle? cellStyle;
@ -113,7 +113,7 @@ class _MultiSelectCellState extends State<MultiSelectCell> {
return _SelectOptionCell(
selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle,
onFocus: (value) => widget.onFocus.value = value,
onFocus: (value) => widget.onCellEditing.value = value,
cellContextBuilder: widget.cellContextBuilder);
},
),

View File

@ -156,6 +156,7 @@ class _TextField extends StatelessWidget {
selectedOptionMap: optionMap,
distanceToText: _editorPannelWidth * 0.7,
tagController: _tagController,
onClick: () => FlowyOverlay.of(context).remove(SelectOptionTypeOptionEditor.identifier),
newText: (text) {
context.read<SelectOptionCellEditorBloc>().add(SelectOptionEditorEvent.filterOption(text));
},
@ -207,6 +208,7 @@ class _CreateOptionCell extends StatelessWidget {
SelectOptionTag(
name: name,
color: theme.shader6,
onSelected: () => context.read<SelectOptionCellEditorBloc>().add(SelectOptionEditorEvent.newOption(name)),
),
],
);
@ -233,7 +235,11 @@ class _SelectOptionCell extends StatelessWidget {
context.read<SelectOptionCellEditorBloc>().add(SelectOptionEditorEvent.selectOption(option.id));
},
children: [
if (isSelected) svgWidget("grid/checkmark"),
if (isSelected)
Padding(
padding: const EdgeInsets.only(right: 6),
child: svgWidget("grid/checkmark"),
),
],
),
),

View File

@ -22,6 +22,7 @@ class SelectOptionTextField extends StatelessWidget {
final Function(String) onNewTag;
final Function(String) newText;
final VoidCallback? onClick;
SelectOptionTextField({
required this.options,
@ -30,6 +31,7 @@ class SelectOptionTextField extends StatelessWidget {
required this.tagController,
required this.onNewTag,
required this.newText,
this.onClick,
TextEditingController? controller,
FocusNode? focusNode,
Key? key,
@ -53,6 +55,7 @@ class SelectOptionTextField extends StatelessWidget {
autofocus: true,
controller: editController,
focusNode: focusNode,
onTap: onClick,
onChanged: (text) {
if (onChanged != null) {
onChanged(text);

View File

@ -13,7 +13,7 @@ class GridTextCellStyle extends GridCellStyle {
});
}
class GridTextCell extends StatefulWidget with GridCellWidget {
class GridTextCell extends GridCellWidget {
final GridCellContextBuilder cellContextBuilder;
late final GridTextCellStyle? cellStyle;
GridTextCell({
@ -29,13 +29,12 @@ class GridTextCell extends StatefulWidget with GridCellWidget {
}
@override
State<GridTextCell> createState() => _GridTextCellState();
GridFocusNodeCellState<GridTextCell> createState() => _GridTextCellState();
}
class _GridTextCellState extends State<GridTextCell> {
class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
late TextCellBloc _cellBloc;
late TextEditingController _controller;
late CellSingleFocusNode _focusNode;
Timer? _delayOperation;
@override
@ -44,10 +43,6 @@ class _GridTextCellState extends State<GridTextCell> {
_cellBloc = getIt<TextCellBloc>(param1: cellContext);
_cellBloc.add(const TextCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.content);
_focusNode = CellSingleFocusNode();
_listenFocusNode();
_listenRequestFocus(context);
super.initState();
}
@ -63,9 +58,9 @@ class _GridTextCellState extends State<GridTextCell> {
},
child: TextField(
controller: _controller,
focusNode: _focusNode,
focusNode: focusNode,
onChanged: (value) => focusChanged(),
onEditingComplete: () => _focusNode.unfocus(),
onEditingComplete: () => focusNode.unfocus(),
maxLines: null,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
decoration: InputDecoration(
@ -81,39 +76,12 @@ class _GridTextCellState extends State<GridTextCell> {
@override
Future<void> dispose() async {
widget.requestFocus.removeAllListener();
_delayOperation?.cancel();
_cellBloc.close();
_focusNode.removeSingleListener();
_focusNode.dispose();
super.dispose();
}
@override
void didUpdateWidget(covariant GridTextCell oldWidget) {
if (oldWidget != widget) {
_listenFocusNode();
}
super.didUpdateWidget(oldWidget);
}
void _listenFocusNode() {
widget.onFocus.value = _focusNode.hasFocus;
_focusNode.setSingleListener(() {
widget.onFocus.value = _focusNode.hasFocus;
focusChanged();
});
}
void _listenRequestFocus(BuildContext context) {
widget.requestFocus.addListener(() {
if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
FocusScope.of(context).requestFocus(_focusNode);
}
});
}
Future<void> focusChanged() async {
if (mounted) {
_delayOperation?.cancel();
@ -124,4 +92,12 @@ class _GridTextCellState extends State<GridTextCell> {
});
}
}
@override
String? onCopy() => _cellBloc.state.content;
@override
void onInsert(String value) {
_cellBloc.add(TextCellEvent.updateText(value));
}
}

View File

@ -6,9 +6,10 @@ import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
class URLCellEditor extends StatefulWidget {
class URLCellEditor extends StatefulWidget with FlowyOverlayDelegate {
final GridURLCellContext cellContext;
const URLCellEditor({required this.cellContext, Key? key}) : super(key: key);
final VoidCallback completed;
const URLCellEditor({required this.cellContext, required this.completed, Key? key}) : super(key: key);
@override
State<URLCellEditor> createState() => _URLCellEditorState();
@ -16,27 +17,43 @@ class URLCellEditor extends StatefulWidget {
static void show(
BuildContext context,
GridURLCellContext cellContext,
VoidCallback completed,
) {
FlowyOverlay.of(context).remove(identifier());
final editor = URLCellEditor(
cellContext: cellContext,
completed: completed,
);
//
FlowyOverlay.of(context).insertWithAnchor(
widget: OverlayContainer(
child: SizedBox(width: 200, child: editor),
child: SizedBox(
width: 200,
child: Padding(padding: const EdgeInsets.all(6), child: editor),
),
constraints: BoxConstraints.loose(const Size(300, 160)),
),
identifier: URLCellEditor.identifier(),
anchorContext: context,
anchorDirection: AnchorDirection.bottomWithCenterAligned,
delegate: editor,
);
}
static String identifier() {
return (URLCellEditor).toString();
}
@override
bool asBarrier() {
return true;
}
@override
void didRemove() {
completed();
}
}
class _URLCellEditorState extends State<URLCellEditor> {

View File

@ -1,10 +1,13 @@
import 'dart:async';
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/workspace/application/grid/cell/url_cell_bloc.dart';
import 'package:app_flowy/workspace/presentation/home/toast.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_accessory.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:app_flowy/workspace/application/grid/prelude.dart';
import 'package:url_launcher/url_launcher.dart';
@ -14,12 +17,20 @@ import 'cell_editor.dart';
class GridURLCellStyle extends GridCellStyle {
String? placeholder;
List<GridURLCellAccessoryType> accessoryTypes;
GridURLCellStyle({
this.placeholder,
this.accessoryTypes = const [],
});
}
class GridURLCell extends StatefulWidget with GridCellWidget {
enum GridURLCellAccessoryType {
edit,
copyURL,
}
class GridURLCell extends GridCellWidget {
final GridCellContextBuilder cellContextBuilder;
late final GridURLCellStyle? cellStyle;
GridURLCell({
@ -35,10 +46,39 @@ class GridURLCell extends StatefulWidget with GridCellWidget {
}
@override
State<GridURLCell> createState() => _GridURLCellState();
GridCellState<GridURLCell> createState() => _GridURLCellState();
GridCellAccessory accessoryFromType(GridURLCellAccessoryType ty, GridCellAccessoryBuildContext buildContext) {
switch (ty) {
case GridURLCellAccessoryType.edit:
final cellContext = cellContextBuilder.build() as GridURLCellContext;
return _EditURLAccessory(cellContext: cellContext, anchorContext: buildContext.anchorContext);
case GridURLCellAccessoryType.copyURL:
final cellContext = cellContextBuilder.build() as GridURLCellContext;
return _CopyURLAccessory(cellContext: cellContext);
}
}
class _GridURLCellState extends State<GridURLCell> {
@override
List<GridCellAccessory> Function(GridCellAccessoryBuildContext buildContext) get accessoryBuilder => (buildContext) {
final List<GridCellAccessory> accessories = [];
if (cellStyle != null) {
accessories.addAll(cellStyle!.accessoryTypes.map((ty) {
return accessoryFromType(ty, buildContext);
}));
}
// If the accessories is empty then the default accessory will be GridURLCellAccessoryType.edit
if (accessories.isEmpty) {
accessories.add(accessoryFromType(GridURLCellAccessoryType.edit, buildContext));
}
return accessories;
};
}
class _GridURLCellState extends GridCellState<GridURLCell> {
late URLCellBloc _cellBloc;
@override
@ -46,7 +86,6 @@ class _GridURLCellState extends State<GridURLCell> {
final cellContext = widget.cellContextBuilder.build() as GridURLCellContext;
_cellBloc = URLCellBloc(cellContext: cellContext);
_cellBloc.add(const URLCellEvent.initial());
_listenRequestFocus(context);
super.initState();
}
@ -66,14 +105,17 @@ class _GridURLCellState extends State<GridURLCell> {
fontSize: 14,
decoration: TextDecoration.underline,
),
recognizer: _tapGesture(context),
),
);
return CellEnterRegion(
return SizedBox.expand(
child: GestureDetector(
child: Align(alignment: Alignment.centerLeft, child: richText),
expander: _EditCellIndicator(onTap: () {}),
);
onTap: () async {
final url = context.read<URLCellBloc>().state.url;
await _openUrlOrEdit(url);
},
));
},
),
);
@ -81,51 +123,72 @@ class _GridURLCellState extends State<GridURLCell> {
@override
Future<void> dispose() async {
widget.requestFocus.removeAllListener();
_cellBloc.close();
super.dispose();
}
TapGestureRecognizer _tapGesture(BuildContext context) {
final gesture = TapGestureRecognizer();
gesture.onTap = () async {
final url = context.read<URLCellBloc>().state.url;
await _openUrlOrEdit(url);
};
return gesture;
}
Future<void> _openUrlOrEdit(String url) async {
final uri = Uri.parse(url);
if (url.isNotEmpty && await canLaunchUrl(uri)) {
await launchUrl(uri);
} else {
final cellContext = widget.cellContextBuilder.build() as GridURLCellContext;
URLCellEditor.show(context, cellContext);
}
}
void _listenRequestFocus(BuildContext context) {
widget.requestFocus.addListener(() {
_openUrlOrEdit(_cellBloc.state.url);
widget.onCellEditing.value = true;
URLCellEditor.show(context, cellContext, () {
widget.onCellEditing.value = false;
});
}
}
class _EditCellIndicator extends StatelessWidget {
final VoidCallback onTap;
const _EditCellIndicator({required this.onTap, Key? key}) : super(key: key);
@override
void requestBeginFocus() {
_openUrlOrEdit(_cellBloc.state.url);
}
@override
String? onCopy() => _cellBloc.state.content;
@override
void onInsert(String value) {
_cellBloc.add(URLCellEvent.updateURL(value));
}
}
class _EditURLAccessory extends StatelessWidget with GridCellAccessory {
final GridURLCellContext cellContext;
final BuildContext anchorContext;
const _EditURLAccessory({
required this.cellContext,
required this.anchorContext,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return FlowyIconButton(
width: 26,
onPressed: onTap,
hoverColor: theme.hover,
radius: BorderRadius.circular(4),
iconPadding: const EdgeInsets.all(5),
icon: svgWidget("editor/edit", color: theme.iconColor),
);
return svgWidget("editor/edit", color: theme.iconColor);
}
@override
void onTap() {
URLCellEditor.show(anchorContext, cellContext, () {});
}
}
class _CopyURLAccessory extends StatelessWidget with GridCellAccessory {
final GridURLCellContext cellContext;
const _CopyURLAccessory({required this.cellContext, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return svgWidget("editor/copy", color: theme.iconColor);
}
@override
void onTap() {
final content = cellContext.getCellData(loadIfNoCache: false)?.content ?? "";
Clipboard.setData(ClipboardData(text: content));
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
}
}

View File

@ -6,7 +6,6 @@ import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show Field;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -24,6 +23,7 @@ class GridFieldCell extends StatelessWidget {
return BlocProvider(
create: (context) => FieldCellBloc(cellContext: cellContext)..add(const FieldCellEvent.initial()),
child: BlocBuilder<FieldCellBloc, FieldCellState>(
// buildWhen: (p, c) => p.field != c.field,
builder: (context, state) {
final button = FieldCellButton(
field: state.field,
@ -38,7 +38,7 @@ class GridFieldCell extends StatelessWidget {
);
return _GridHeaderCellContainer(
width: state.field.width.toDouble(),
width: state.width,
child: Stack(
alignment: Alignment.centerRight,
fit: StackFit.expand,
@ -60,13 +60,14 @@ class GridFieldCell extends StatelessWidget {
void _showFieldEditor(BuildContext context) {
final state = context.read<FieldCellBloc>().state;
final field = state.field;
FieldEditor(
gridId: state.gridId,
fieldName: state.field.name,
fieldName: field.name,
contextLoader: FieldContextLoader(
gridId: state.gridId,
field: state.field,
field: field,
),
).show(context);
}
@ -84,7 +85,7 @@ class _GridHeaderCellContainer extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
final borderSide = BorderSide(color: theme.shader4, width: 0.4);
final borderSide = BorderSide(color: theme.shader5, width: 1.0);
final decoration = BoxDecoration(
border: Border(
top: borderSide,
@ -113,21 +114,19 @@ class _DragToExpandLine extends StatelessWidget {
onTap: () {},
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onHorizontalDragCancel: () {},
onHorizontalDragUpdate: (value) {
// context.read<FieldCellBloc>().add(FieldCellEvent.updateWidth(value.delta.dx));
Log.info(value);
context.read<FieldCellBloc>().add(FieldCellEvent.startUpdateWidth(value.delta.dx));
},
onHorizontalDragEnd: (end) {
Log.info(end);
context.read<FieldCellBloc>().add(const FieldCellEvent.endUpdateWidth());
},
child: FlowyHover(
style: HoverStyle(
hoverColor: theme.main1,
borderRadius: BorderRadius.zero,
contentMargin: const EdgeInsets.only(left: 5),
contentMargin: const EdgeInsets.only(left: 6),
),
child: const SizedBox(width: 2),
child: const SizedBox(width: 4),
),
),
);

View File

@ -9,7 +9,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.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/spacing.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/format.pbenum.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:easy_localization/easy_localization.dart' hide NumberFormat;

View File

@ -25,6 +25,8 @@ class SelectOptionTypeOptionEditor extends StatelessWidget {
Key? key,
}) : super(key: key);
static String get identifier => (SelectOptionTypeOptionEditor).toString();
@override
Widget build(BuildContext context) {
return BlocProvider(

View File

@ -1,5 +1,7 @@
import 'package:app_flowy/workspace/application/grid/prelude.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_accessory.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_cotainer.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/prelude.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart';
@ -170,16 +172,29 @@ class _RowCells extends StatelessWidget {
List<Widget> _makeCells(BuildContext context, GridCellMap gridCellMap) {
return gridCellMap.values.map(
(gridCell) {
Widget? expander;
final GridCellWidget child = buildGridCellWidget(gridCell, cellCache);
accessoryBuilder(GridCellAccessoryBuildContext buildContext) {
final builder = child.accessoryBuilder;
List<GridCellAccessory> accessories = [];
if (gridCell.field.isPrimary) {
expander = _CellExpander(onExpand: onExpand);
accessories.add(PrimaryCellAccessory(
onTapCallback: onExpand,
isCellEditing: buildContext.isCellEditing,
));
}
if (builder != null) {
accessories.addAll(builder(buildContext));
}
return accessories;
}
return CellContainer(
width: gridCell.field.width.toDouble(),
child: buildGridCellWidget(gridCell, cellCache),
child: child,
rowStateNotifier: Provider.of<RegionStateNotifier>(context, listen: false),
expander: expander,
accessoryBuilder: accessoryBuilder,
);
},
).toList();
@ -199,26 +214,6 @@ class RegionStateNotifier extends ChangeNotifier {
bool get onEnter => _onEnter;
}
class _CellExpander extends StatelessWidget {
final VoidCallback onExpand;
const _CellExpander({required this.onExpand, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return FittedBox(
fit: BoxFit.contain,
child: FlowyIconButton(
width: 26,
onPressed: onExpand,
iconPadding: const EdgeInsets.all(5),
radius: BorderRadius.circular(4),
icon: svgWidget("grid/expander", color: theme.main1),
),
);
}
}
class _RowEnterRegion extends StatefulWidget {
final Widget child;
const _RowEnterRegion({required this.child, Key? key}) : super(key: key);

View File

@ -1,44 +0,0 @@
import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/workspace/application/grid/prelude.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class NumberCell extends StatefulWidget {
final GridCell cellData;
const NumberCell({
required this.cellData,
Key? key,
}) : super(key: key);
@override
State<NumberCell> createState() => _NumberCellState();
}
class _NumberCellState extends State<NumberCell> {
late NumberCellBloc _cellBloc;
@override
void initState() {
_cellBloc = getIt<NumberCellBloc>(param1: widget.cellData);
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<NumberCellBloc, NumberCellState>(
builder: (context, state) {
return Container();
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -24,6 +24,7 @@ class GridRowActionSheet extends StatelessWidget {
child: BlocBuilder<RowActionSheetBloc, RowActionSheetState>(
builder: (context, state) {
final cells = _RowAction.values
.where((value) => value.enable())
.map(
(action) => _RowActionCell(
action: action,

View File

@ -3,6 +3,7 @@ import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
import 'package:app_flowy/workspace/application/grid/row/row_detail_bloc.dart';
import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_accessory.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/prelude.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/url_cell.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart';
@ -10,7 +11,6 @@ import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
@ -149,12 +149,18 @@ class _RowDetailCell extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
final style = _customCellStyle(theme, gridCell.field.fieldType);
final cell = buildGridCellWidget(gridCell, cellCache, style: style);
final cell = buildGridCellWidget(
gridCell,
cellCache,
style: _buildCellStyle(theme, gridCell.field.fieldType),
final gesture = GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => cell.beginFocus.notify(),
child: AccessoryHover(
child: cell,
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
),
);
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 40),
child: IntrinsicHeight(
@ -167,12 +173,7 @@ class _RowDetailCell extends StatelessWidget {
child: FieldCellButton(field: gridCell.field, onTap: () => _showFieldEditor(context)),
),
const HSpace(10),
Expanded(
child: FlowyHover2(
child: cell,
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
),
),
Expanded(child: gesture),
],
),
),
@ -191,7 +192,7 @@ class _RowDetailCell extends StatelessWidget {
}
}
GridCellStyle? _buildCellStyle(AppTheme theme, FieldType fieldType) {
GridCellStyle? _customCellStyle(AppTheme theme, FieldType fieldType) {
switch (fieldType) {
case FieldType.Checkbox:
return null;
@ -217,7 +218,11 @@ GridCellStyle? _buildCellStyle(AppTheme theme, FieldType fieldType) {
case FieldType.URL:
return GridURLCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
accessoryTypes: [
GridURLCellAccessoryType.edit,
GridURLCellAccessoryType.copyURL,
],
);
}
return null;
throw UnimplementedError;
}

View File

@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class GridShortcuts extends StatelessWidget {
final Widget child;
const GridShortcuts({required this.child, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: bindKeys([]),
child: Actions(
dispatcher: LoggingActionDispatcher(),
actions: const {},
child: child,
),
);
}
}
Map<ShortcutActivator, Intent> bindKeys(List<LogicalKeyboardKey> keys) {
return {for (var key in keys) LogicalKeySet(key): KeyboardKeyIdent(key)};
}
Map<Type, Action<Intent>> bindActions() {
return {
KeyboardKeyIdent: KeyboardBindingAction(),
};
}
class KeyboardKeyIdent extends Intent {
final KeyboardKey key;
const KeyboardKeyIdent(this.key);
}
class KeyboardBindingAction extends Action<KeyboardKeyIdent> {
KeyboardBindingAction();
@override
void invoke(covariant KeyboardKeyIdent intent) {
// print(intent);
}
}
class LoggingActionDispatcher extends ActionDispatcher {
@override
Object? invokeAction(
covariant Action<Intent> action,
covariant Intent intent, [
BuildContext? context,
]) {
// print('Action invoked: $action($intent) from $context');
super.invokeAction(action, intent, context);
return null;
}
}

View File

@ -85,7 +85,7 @@ class GridSettingList extends StatelessWidget {
}
Widget _renderList() {
final cells = GridSettingAction.values.map((action) {
final cells = GridSettingAction.values.where((value) => value.enable()).map((action) {
return _SettingItem(action: action);
}).toList();

View File

@ -27,7 +27,7 @@ class _EmojiStyleButtonState extends State<FlowyEmojiStyleButton> {
bool _isToggled = false;
// Style get _selectionStyle => widget.controller.getSelectionStyle();
final GlobalKey emojiButtonKey = GlobalKey();
OverlayEntry _entry = OverlayEntry(builder: (context) => Container());
OverlayEntry? _entry;
// final FocusNode _keyFocusNode = FocusNode();
@override
@ -52,6 +52,12 @@ class _EmojiStyleButtonState extends State<FlowyEmojiStyleButton> {
);
}
@override
void dispose() {
_entry?.remove();
super.dispose();
}
// @override
// void didUpdateWidget(covariant FlowyEmojiStyleButton oldWidget) {
// super.didUpdateWidget(oldWidget);
@ -77,8 +83,9 @@ class _EmojiStyleButtonState extends State<FlowyEmojiStyleButton> {
// }
void _toggleAttribute() {
if (_entry.mounted) {
_entry.remove();
if (_entry?.mounted ?? false) {
_entry?.remove();
_entry = null;
setState(() => _isToggled = false);
} else {
RenderBox box = emojiButtonKey.currentContext?.findRenderObject() as RenderBox;
@ -93,7 +100,7 @@ class _EmojiStyleButtonState extends State<FlowyEmojiStyleButton> {
),
);
Overlay.of(context)!.insert(_entry);
Overlay.of(context)!.insert(_entry!);
setState(() => _isToggled = true);
}

View File

@ -1,5 +1,5 @@
import 'package:app_flowy/startup/tasks/rust_sdk.dart';
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
import 'package:app_flowy/workspace/presentation/home/toast.dart';
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme.dart';
@ -16,7 +16,6 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:fluttertoast/fluttertoast.dart';
class QuestionBubble extends StatelessWidget {
const QuestionBubble({Key? key}) : super(key: key);
@ -46,7 +45,7 @@ class QuestionBubble extends StatelessWidget {
_launchURL("https://discord.gg/9Q2xaN37tV");
break;
case BubbleAction.debug:
const _DebugToast().show();
_DebugToast().show();
break;
}
});
@ -71,55 +70,14 @@ class QuestionBubble extends StatelessWidget {
}
}
class _DebugToast extends StatelessWidget {
const _DebugToast({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: Future(() async {
class _DebugToast {
void show() async {
var debugInfo = "";
debugInfo += await _getDeviceInfo();
debugInfo += await _getDocumentPath();
Clipboard.setData(ClipboardData(text: debugInfo));
}),
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return _done(context, Text("Error: ${snapshot.error}"));
} else {
return _done(context, null);
}
} else {
return const CircularProgressIndicator();
}
},
);
}
Widget _done(BuildContext context, Widget? error) {
final theme = context.watch<AppTheme>();
return Container(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(25.0), color: theme.main1),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.check),
const SizedBox(width: 12.0),
(error == null) ? Text(LocaleKeys.questionBubble_debug_success.tr()) : error
],
),
);
}
void show() {
fToast.showToast(
child: this,
gravity: ToastGravity.BOTTOM,
toastDuration: const Duration(seconds: 3),
);
showMessageToast(LocaleKeys.questionBubble_debug_success.tr());
}
Future<String> _getDeviceInfo() async {

View File

@ -26,6 +26,23 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe
flutter_macos_podfile_setup
def build_specify_archs_only
if ENV.has_key?('BUILD_ARCHS')
xcodeproj_path = File.dirname(__FILE__) + '/Runner.xcodeproj'
project = Xcodeproj::Project.open(xcodeproj_path)
project.targets.each do |target|
if target.name == 'Runner'
target.build_configurations.each do |config|
config.build_settings['ARCHS'] = ENV['BUILD_ARCHS']
end
end
end
project.save()
end
end
build_specify_archs_only()
target 'Runner' do
use_frameworks!
use_modular_headers!

View File

@ -421,7 +421,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
EXCLUDED_ARCHS = arm64;
EXCLUDED_ARCHS = "";
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -553,7 +553,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
EXCLUDED_ARCHS = arm64;
EXCLUDED_ARCHS = "";
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -577,7 +577,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
EXCLUDED_ARCHS = arm64;
EXCLUDED_ARCHS = "";
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",

View File

@ -71,7 +71,7 @@ class TextStyles {
static TextStyle get CalloutFocus => Callout.bold;
// ignore: non_constant_identifier_names
static TextStyle get Btn => quicksand.bold.size(FontSizes.s14).letterSpace(1.75);
static TextStyle get Btn => quicksand.bold.size(FontSizes.s16).letterSpace(1.75);
// ignore: non_constant_identifier_names
static TextStyle get BtnSelected => quicksand.size(FontSizes.s14).letterSpace(1.75);

View File

@ -1,4 +1,3 @@
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
@ -28,7 +27,7 @@ class FlowyButton extends StatelessWidget {
return InkWell(
onTap: onTap,
child: FlowyHover(
style: HoverStyle(borderRadius: Corners.s6Border, hoverColor: hoverColor),
style: HoverStyle(borderRadius: BorderRadius.zero, hoverColor: hoverColor),
setSelected: () => isSelected,
builder: (context, onHover) => _render(),
),

View File

@ -1,9 +1,6 @@
import 'package:flutter/material.dart';
// ignore: unused_import
import 'package:flowy_infra/time/duration.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme.dart';
import 'package:provider/provider.dart';
typedef HoverBuilder = Widget Function(BuildContext context, bool onHover);
@ -52,7 +49,7 @@ class _FlowyHoverState extends State<FlowyHover> {
child: child,
);
} else {
return child;
return Container(child: child, color: widget.style.backgroundColor);
}
}
}
@ -63,12 +60,14 @@ class HoverStyle {
final Color hoverColor;
final BorderRadius borderRadius;
final EdgeInsets contentMargin;
final Color backgroundColor;
const HoverStyle(
{this.borderColor = Colors.transparent,
this.borderWidth = 0,
this.borderRadius = const BorderRadius.all(Radius.circular(6)),
this.contentMargin = EdgeInsets.zero,
this.backgroundColor = Colors.transparent,
required this.hoverColor});
}
@ -100,120 +99,3 @@ class FlowyHoverContainer extends StatelessWidget {
);
}
}
//
abstract class FlowyHoverWidget extends Widget {
const FlowyHoverWidget({Key? key}) : super(key: key);
ValueNotifier<bool>? get onFocus;
}
class FlowyHover2 extends StatefulWidget {
final FlowyHoverWidget child;
final EdgeInsets contentPadding;
const FlowyHover2({
required this.child,
this.contentPadding = EdgeInsets.zero,
Key? key,
}) : super(key: key);
@override
State<FlowyHover2> createState() => _FlowyHover2State();
}
class _FlowyHover2State extends State<FlowyHover2> {
late FlowyHoverState _hoverState;
VoidCallback? _listenerFn;
@override
void initState() {
_hoverState = FlowyHoverState();
listener() {
_hoverState.onFocus = widget.child.onFocus?.value ?? false;
}
_listenerFn = listener;
widget.child.onFocus?.addListener(listener);
super.initState();
}
@override
void dispose() {
_hoverState.dispose();
if (_listenerFn != null) {
widget.child.onFocus?.removeListener(_listenerFn!);
_listenerFn = null;
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: _hoverState,
child: MouseRegion(
cursor: SystemMouseCursors.click,
opaque: false,
onEnter: (p) => setState(() => _hoverState.onHover = true),
onExit: (p) => setState(() => _hoverState.onHover = false),
child: Stack(
fit: StackFit.loose,
alignment: AlignmentDirectional.center,
children: [
const _HoverBackground(),
Padding(
padding: widget.contentPadding,
child: widget.child,
),
],
),
),
);
}
}
class _HoverBackground extends StatelessWidget {
const _HoverBackground({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return Consumer<FlowyHoverState>(
builder: (context, state, child) {
if (state.onHover || state.onFocus) {
return FlowyHoverContainer(
style: HoverStyle(borderRadius: Corners.s6Border, hoverColor: theme.shader6),
);
} else {
return const SizedBox();
}
},
);
}
}
class FlowyHoverState extends ChangeNotifier {
bool _onHover = false;
bool _onFocus = false;
set onHover(bool value) {
if (_onHover != value) {
_onHover = value;
notifyListeners();
}
}
bool get onHover => _onHover;
set onFocus(bool value) {
if (_onFocus != value) {
_onFocus = value;
notifyListeners();
}
}
bool get onFocus => _onFocus;
}

View File

@ -23,7 +23,7 @@ class StyledSingleChildScrollView extends StatefulWidget {
this.handleColor,
this.controller,
this.scrollbarPadding,
this.barSize = 6,
this.barSize = 12,
}) : super(key: key);
@override

View File

@ -15,7 +15,7 @@ class PrimaryTextButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
TextStyle txtStyle = TextStyles.Footnote.textColor(Colors.white);
TextStyle txtStyle = TextStyles.Btn.textColor(Colors.white);
return PrimaryButton(bigMode: bigMode, onPressed: onPressed, child: Text(label, style: txtStyle));
}
}

View File

@ -17,7 +17,7 @@ class SecondaryTextButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
TextStyle txtStyle = TextStyles.Footnote.textColor(theme.main1);
TextStyle txtStyle = TextStyles.Btn.textColor(theme.main1);
return SecondaryButton(bigMode: bigMode, onPressed: onPressed, child: Text(label, style: txtStyle));
}
}

View File

@ -1,13 +1,13 @@
/// Auto generate. Do not edit
part of '../../dispatch.dart';
class BlockEventGetBlockData {
class TextBlockEventGetBlockData {
TextBlockId request;
BlockEventGetBlockData(this.request);
TextBlockEventGetBlockData(this.request);
Future<Either<TextBlockDelta, FlowyError>> send() {
final request = FFIRequest.create()
..event = BlockEvent.GetBlockData.toString()
..event = TextBlockEvent.GetBlockData.toString()
..payload = requestToBytes(this.request);
return Dispatch.asyncRequest(request)
@ -18,13 +18,13 @@ class BlockEventGetBlockData {
}
}
class BlockEventApplyDelta {
class TextBlockEventApplyDelta {
TextBlockDelta request;
BlockEventApplyDelta(this.request);
TextBlockEventApplyDelta(this.request);
Future<Either<TextBlockDelta, FlowyError>> send() {
final request = FFIRequest.create()
..event = BlockEvent.ApplyDelta.toString()
..event = TextBlockEvent.ApplyDelta.toString()
..payload = requestToBytes(this.request);
return Dispatch.asyncRequest(request)
@ -35,13 +35,13 @@ class BlockEventApplyDelta {
}
}
class BlockEventExportDocument {
class TextBlockEventExportDocument {
ExportPayload request;
BlockEventExportDocument(this.request);
TextBlockEventExportDocument(this.request);
Future<Either<ExportData, FlowyError>> send() {
final request = FFIRequest.create()
..event = BlockEvent.ExportDocument.toString()
..event = TextBlockEvent.ExportDocument.toString()
..payload = requestToBytes(this.request);
return Dispatch.asyncRequest(request)

View File

@ -0,0 +1,11 @@
///
// Generated code. Do not modify.
// source: format.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
import 'dart:core' as $core;
export 'format.pbenum.dart';

View File

@ -0,0 +1,94 @@
///
// Generated code. Do not modify.
// source: format.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
// ignore_for_file: UNDEFINED_SHOWN_NAME
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class NumberFormat extends $pb.ProtobufEnum {
static const NumberFormat Number = NumberFormat._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Number');
static const NumberFormat USD = NumberFormat._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'USD');
static const NumberFormat CanadianDollar = NumberFormat._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CanadianDollar');
static const NumberFormat EUR = NumberFormat._(4, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'EUR');
static const NumberFormat Pound = NumberFormat._(5, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Pound');
static const NumberFormat Yen = NumberFormat._(6, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Yen');
static const NumberFormat Ruble = NumberFormat._(7, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Ruble');
static const NumberFormat Rupee = NumberFormat._(8, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Rupee');
static const NumberFormat Won = NumberFormat._(9, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Won');
static const NumberFormat Yuan = NumberFormat._(10, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Yuan');
static const NumberFormat Real = NumberFormat._(11, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Real');
static const NumberFormat Lira = NumberFormat._(12, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Lira');
static const NumberFormat Rupiah = NumberFormat._(13, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Rupiah');
static const NumberFormat Franc = NumberFormat._(14, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Franc');
static const NumberFormat HongKongDollar = NumberFormat._(15, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'HongKongDollar');
static const NumberFormat NewZealandDollar = NumberFormat._(16, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'NewZealandDollar');
static const NumberFormat Krona = NumberFormat._(17, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Krona');
static const NumberFormat NorwegianKrone = NumberFormat._(18, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'NorwegianKrone');
static const NumberFormat MexicanPeso = NumberFormat._(19, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'MexicanPeso');
static const NumberFormat Rand = NumberFormat._(20, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Rand');
static const NumberFormat NewTaiwanDollar = NumberFormat._(21, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'NewTaiwanDollar');
static const NumberFormat DanishKrone = NumberFormat._(22, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DanishKrone');
static const NumberFormat Baht = NumberFormat._(23, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Baht');
static const NumberFormat Forint = NumberFormat._(24, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Forint');
static const NumberFormat Koruna = NumberFormat._(25, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Koruna');
static const NumberFormat Shekel = NumberFormat._(26, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Shekel');
static const NumberFormat ChileanPeso = NumberFormat._(27, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ChileanPeso');
static const NumberFormat PhilippinePeso = NumberFormat._(28, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PhilippinePeso');
static const NumberFormat Dirham = NumberFormat._(29, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Dirham');
static const NumberFormat ColombianPeso = NumberFormat._(30, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ColombianPeso');
static const NumberFormat Riyal = NumberFormat._(31, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Riyal');
static const NumberFormat Ringgit = NumberFormat._(32, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Ringgit');
static const NumberFormat Leu = NumberFormat._(33, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Leu');
static const NumberFormat ArgentinePeso = NumberFormat._(34, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ArgentinePeso');
static const NumberFormat UruguayanPeso = NumberFormat._(35, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'UruguayanPeso');
static const NumberFormat Percent = NumberFormat._(36, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Percent');
static const $core.List<NumberFormat> values = <NumberFormat> [
Number,
USD,
CanadianDollar,
EUR,
Pound,
Yen,
Ruble,
Rupee,
Won,
Yuan,
Real,
Lira,
Rupiah,
Franc,
HongKongDollar,
NewZealandDollar,
Krona,
NorwegianKrone,
MexicanPeso,
Rand,
NewTaiwanDollar,
DanishKrone,
Baht,
Forint,
Koruna,
Shekel,
ChileanPeso,
PhilippinePeso,
Dirham,
ColombianPeso,
Riyal,
Ringgit,
Leu,
ArgentinePeso,
UruguayanPeso,
Percent,
];
static final $core.Map<$core.int, NumberFormat> _byValue = $pb.ProtobufEnum.initByValue(values);
static NumberFormat? valueOf($core.int value) => _byValue[value];
const NumberFormat._($core.int v, $core.String n) : super(v, n);
}

View File

@ -0,0 +1,55 @@
///
// Generated code. Do not modify.
// source: format.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
import 'dart:core' as $core;
import 'dart:convert' as $convert;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use numberFormatDescriptor instead')
const NumberFormat$json = const {
'1': 'NumberFormat',
'2': const [
const {'1': 'Number', '2': 0},
const {'1': 'USD', '2': 1},
const {'1': 'CanadianDollar', '2': 2},
const {'1': 'EUR', '2': 4},
const {'1': 'Pound', '2': 5},
const {'1': 'Yen', '2': 6},
const {'1': 'Ruble', '2': 7},
const {'1': 'Rupee', '2': 8},
const {'1': 'Won', '2': 9},
const {'1': 'Yuan', '2': 10},
const {'1': 'Real', '2': 11},
const {'1': 'Lira', '2': 12},
const {'1': 'Rupiah', '2': 13},
const {'1': 'Franc', '2': 14},
const {'1': 'HongKongDollar', '2': 15},
const {'1': 'NewZealandDollar', '2': 16},
const {'1': 'Krona', '2': 17},
const {'1': 'NorwegianKrone', '2': 18},
const {'1': 'MexicanPeso', '2': 19},
const {'1': 'Rand', '2': 20},
const {'1': 'NewTaiwanDollar', '2': 21},
const {'1': 'DanishKrone', '2': 22},
const {'1': 'Baht', '2': 23},
const {'1': 'Forint', '2': 24},
const {'1': 'Koruna', '2': 25},
const {'1': 'Shekel', '2': 26},
const {'1': 'ChileanPeso', '2': 27},
const {'1': 'PhilippinePeso', '2': 28},
const {'1': 'Dirham', '2': 29},
const {'1': 'ColombianPeso', '2': 30},
const {'1': 'Riyal', '2': 31},
const {'1': 'Ringgit', '2': 32},
const {'1': 'Leu', '2': 33},
const {'1': 'ArgentinePeso', '2': 34},
const {'1': 'UruguayanPeso', '2': 35},
const {'1': 'Percent', '2': 36},
],
};
/// Descriptor for `NumberFormat`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List numberFormatDescriptor = $convert.base64Decode('CgxOdW1iZXJGb3JtYXQSCgoGTnVtYmVyEAASBwoDVVNEEAESEgoOQ2FuYWRpYW5Eb2xsYXIQAhIHCgNFVVIQBBIJCgVQb3VuZBAFEgcKA1llbhAGEgkKBVJ1YmxlEAcSCQoFUnVwZWUQCBIHCgNXb24QCRIICgRZdWFuEAoSCAoEUmVhbBALEggKBExpcmEQDBIKCgZSdXBpYWgQDRIJCgVGcmFuYxAOEhIKDkhvbmdLb25nRG9sbGFyEA8SFAoQTmV3WmVhbGFuZERvbGxhchAQEgkKBUtyb25hEBESEgoOTm9yd2VnaWFuS3JvbmUQEhIPCgtNZXhpY2FuUGVzbxATEggKBFJhbmQQFBITCg9OZXdUYWl3YW5Eb2xsYXIQFRIPCgtEYW5pc2hLcm9uZRAWEggKBEJhaHQQFxIKCgZGb3JpbnQQGBIKCgZLb3J1bmEQGRIKCgZTaGVrZWwQGhIPCgtDaGlsZWFuUGVzbxAbEhIKDlBoaWxpcHBpbmVQZXNvEBwSCgoGRGlyaGFtEB0SEQoNQ29sb21iaWFuUGVzbxAeEgkKBVJpeWFsEB8SCwoHUmluZ2dpdBAgEgcKA0xldRAhEhEKDUFyZ2VudGluZVBlc28QIhIRCg1VcnVndWF5YW5QZXNvECMSCwoHUGVyY2VudBAk');

View File

@ -0,0 +1,9 @@
///
// Generated code. Do not modify.
// source: format.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
export 'format.pb.dart';

View File

@ -9,13 +9,11 @@ import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
import 'number_type_option.pbenum.dart';
export 'number_type_option.pbenum.dart';
import 'format.pbenum.dart' as $0;
class NumberTypeOption extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'NumberTypeOption', createEmptyInstance: create)
..e<NumberFormat>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'format', $pb.PbFieldType.OE, defaultOrMaker: NumberFormat.Number, valueOf: NumberFormat.valueOf, enumValues: NumberFormat.values)
..e<$0.NumberFormat>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'format', $pb.PbFieldType.OE, defaultOrMaker: $0.NumberFormat.Number, valueOf: $0.NumberFormat.valueOf, enumValues: $0.NumberFormat.values)
..a<$core.int>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'scale', $pb.PbFieldType.OU3)
..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'symbol')
..aOB(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'signPositive')
@ -25,7 +23,7 @@ class NumberTypeOption extends $pb.GeneratedMessage {
NumberTypeOption._() : super();
factory NumberTypeOption({
NumberFormat? format,
$0.NumberFormat? format,
$core.int? scale,
$core.String? symbol,
$core.bool? signPositive,
@ -71,9 +69,9 @@ class NumberTypeOption extends $pb.GeneratedMessage {
static NumberTypeOption? _defaultInstance;
@$pb.TagNumber(1)
NumberFormat get format => $_getN(0);
$0.NumberFormat get format => $_getN(0);
@$pb.TagNumber(1)
set format(NumberFormat v) { setField(1, v); }
set format($0.NumberFormat v) { setField(1, v); }
@$pb.TagNumber(1)
$core.bool hasFormat() => $_has(0);
@$pb.TagNumber(1)

View File

@ -5,90 +5,3 @@
// @dart = 2.12
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
// ignore_for_file: UNDEFINED_SHOWN_NAME
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class NumberFormat extends $pb.ProtobufEnum {
static const NumberFormat Number = NumberFormat._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Number');
static const NumberFormat USD = NumberFormat._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'USD');
static const NumberFormat CanadianDollar = NumberFormat._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CanadianDollar');
static const NumberFormat EUR = NumberFormat._(4, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'EUR');
static const NumberFormat Pound = NumberFormat._(5, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Pound');
static const NumberFormat Yen = NumberFormat._(6, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Yen');
static const NumberFormat Ruble = NumberFormat._(7, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Ruble');
static const NumberFormat Rupee = NumberFormat._(8, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Rupee');
static const NumberFormat Won = NumberFormat._(9, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Won');
static const NumberFormat Yuan = NumberFormat._(10, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Yuan');
static const NumberFormat Real = NumberFormat._(11, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Real');
static const NumberFormat Lira = NumberFormat._(12, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Lira');
static const NumberFormat Rupiah = NumberFormat._(13, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Rupiah');
static const NumberFormat Franc = NumberFormat._(14, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Franc');
static const NumberFormat HongKongDollar = NumberFormat._(15, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'HongKongDollar');
static const NumberFormat NewZealandDollar = NumberFormat._(16, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'NewZealandDollar');
static const NumberFormat Krona = NumberFormat._(17, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Krona');
static const NumberFormat NorwegianKrone = NumberFormat._(18, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'NorwegianKrone');
static const NumberFormat MexicanPeso = NumberFormat._(19, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'MexicanPeso');
static const NumberFormat Rand = NumberFormat._(20, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Rand');
static const NumberFormat NewTaiwanDollar = NumberFormat._(21, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'NewTaiwanDollar');
static const NumberFormat DanishKrone = NumberFormat._(22, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DanishKrone');
static const NumberFormat Baht = NumberFormat._(23, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Baht');
static const NumberFormat Forint = NumberFormat._(24, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Forint');
static const NumberFormat Koruna = NumberFormat._(25, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Koruna');
static const NumberFormat Shekel = NumberFormat._(26, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Shekel');
static const NumberFormat ChileanPeso = NumberFormat._(27, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ChileanPeso');
static const NumberFormat PhilippinePeso = NumberFormat._(28, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PhilippinePeso');
static const NumberFormat Dirham = NumberFormat._(29, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Dirham');
static const NumberFormat ColombianPeso = NumberFormat._(30, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ColombianPeso');
static const NumberFormat Riyal = NumberFormat._(31, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Riyal');
static const NumberFormat Ringgit = NumberFormat._(32, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Ringgit');
static const NumberFormat Leu = NumberFormat._(33, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Leu');
static const NumberFormat ArgentinePeso = NumberFormat._(34, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ArgentinePeso');
static const NumberFormat UruguayanPeso = NumberFormat._(35, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'UruguayanPeso');
static const NumberFormat Percent = NumberFormat._(36, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Percent');
static const $core.List<NumberFormat> values = <NumberFormat> [
Number,
USD,
CanadianDollar,
EUR,
Pound,
Yen,
Ruble,
Rupee,
Won,
Yuan,
Real,
Lira,
Rupiah,
Franc,
HongKongDollar,
NewZealandDollar,
Krona,
NorwegianKrone,
MexicanPeso,
Rand,
NewTaiwanDollar,
DanishKrone,
Baht,
Forint,
Koruna,
Shekel,
ChileanPeso,
PhilippinePeso,
Dirham,
ColombianPeso,
Riyal,
Ringgit,
Leu,
ArgentinePeso,
UruguayanPeso,
Percent,
];
static final $core.Map<$core.int, NumberFormat> _byValue = $pb.ProtobufEnum.initByValue(values);
static NumberFormat? valueOf($core.int value) => _byValue[value];
const NumberFormat._($core.int v, $core.String n) : super(v, n);
}

View File

@ -8,51 +8,6 @@
import 'dart:core' as $core;
import 'dart:convert' as $convert;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use numberFormatDescriptor instead')
const NumberFormat$json = const {
'1': 'NumberFormat',
'2': const [
const {'1': 'Number', '2': 0},
const {'1': 'USD', '2': 1},
const {'1': 'CanadianDollar', '2': 2},
const {'1': 'EUR', '2': 4},
const {'1': 'Pound', '2': 5},
const {'1': 'Yen', '2': 6},
const {'1': 'Ruble', '2': 7},
const {'1': 'Rupee', '2': 8},
const {'1': 'Won', '2': 9},
const {'1': 'Yuan', '2': 10},
const {'1': 'Real', '2': 11},
const {'1': 'Lira', '2': 12},
const {'1': 'Rupiah', '2': 13},
const {'1': 'Franc', '2': 14},
const {'1': 'HongKongDollar', '2': 15},
const {'1': 'NewZealandDollar', '2': 16},
const {'1': 'Krona', '2': 17},
const {'1': 'NorwegianKrone', '2': 18},
const {'1': 'MexicanPeso', '2': 19},
const {'1': 'Rand', '2': 20},
const {'1': 'NewTaiwanDollar', '2': 21},
const {'1': 'DanishKrone', '2': 22},
const {'1': 'Baht', '2': 23},
const {'1': 'Forint', '2': 24},
const {'1': 'Koruna', '2': 25},
const {'1': 'Shekel', '2': 26},
const {'1': 'ChileanPeso', '2': 27},
const {'1': 'PhilippinePeso', '2': 28},
const {'1': 'Dirham', '2': 29},
const {'1': 'ColombianPeso', '2': 30},
const {'1': 'Riyal', '2': 31},
const {'1': 'Ringgit', '2': 32},
const {'1': 'Leu', '2': 33},
const {'1': 'ArgentinePeso', '2': 34},
const {'1': 'UruguayanPeso', '2': 35},
const {'1': 'Percent', '2': 36},
],
};
/// Descriptor for `NumberFormat`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List numberFormatDescriptor = $convert.base64Decode('CgxOdW1iZXJGb3JtYXQSCgoGTnVtYmVyEAASBwoDVVNEEAESEgoOQ2FuYWRpYW5Eb2xsYXIQAhIHCgNFVVIQBBIJCgVQb3VuZBAFEgcKA1llbhAGEgkKBVJ1YmxlEAcSCQoFUnVwZWUQCBIHCgNXb24QCRIICgRZdWFuEAoSCAoEUmVhbBALEggKBExpcmEQDBIKCgZSdXBpYWgQDRIJCgVGcmFuYxAOEhIKDkhvbmdLb25nRG9sbGFyEA8SFAoQTmV3WmVhbGFuZERvbGxhchAQEgkKBUtyb25hEBESEgoOTm9yd2VnaWFuS3JvbmUQEhIPCgtNZXhpY2FuUGVzbxATEggKBFJhbmQQFBITCg9OZXdUYWl3YW5Eb2xsYXIQFRIPCgtEYW5pc2hLcm9uZRAWEggKBEJhaHQQFxIKCgZGb3JpbnQQGBIKCgZLb3J1bmEQGRIKCgZTaGVrZWwQGhIPCgtDaGlsZWFuUGVzbxAbEhIKDlBoaWxpcHBpbmVQZXNvEBwSCgoGRGlyaGFtEB0SEQoNQ29sb21iaWFuUGVzbxAeEgkKBVJpeWFsEB8SCwoHUmluZ2dpdBAgEgcKA0xldRAhEhEKDUFyZ2VudGluZVBlc28QIhIRCg1VcnVndWF5YW5QZXNvECMSCwoHUGVyY2VudBAk');
@$core.Deprecated('Use numberTypeOptionDescriptor instead')
const NumberTypeOption$json = const {
'1': 'NumberTypeOption',

View File

@ -7,6 +7,7 @@ export './row_entities.pb.dart';
export './cell_entities.pb.dart';
export './url_type_option.pb.dart';
export './checkbox_type_option.pb.dart';
export './format.pb.dart';
export './event_map.pb.dart';
export './text_type_option.pb.dart';
export './date_type_option.pb.dart';

View File

@ -9,20 +9,20 @@
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class BlockEvent extends $pb.ProtobufEnum {
static const BlockEvent GetBlockData = BlockEvent._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'GetBlockData');
static const BlockEvent ApplyDelta = BlockEvent._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ApplyDelta');
static const BlockEvent ExportDocument = BlockEvent._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ExportDocument');
class TextBlockEvent extends $pb.ProtobufEnum {
static const TextBlockEvent GetBlockData = TextBlockEvent._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'GetBlockData');
static const TextBlockEvent ApplyDelta = TextBlockEvent._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ApplyDelta');
static const TextBlockEvent ExportDocument = TextBlockEvent._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ExportDocument');
static const $core.List<BlockEvent> values = <BlockEvent> [
static const $core.List<TextBlockEvent> values = <TextBlockEvent> [
GetBlockData,
ApplyDelta,
ExportDocument,
];
static final $core.Map<$core.int, BlockEvent> _byValue = $pb.ProtobufEnum.initByValue(values);
static BlockEvent? valueOf($core.int value) => _byValue[value];
static final $core.Map<$core.int, TextBlockEvent> _byValue = $pb.ProtobufEnum.initByValue(values);
static TextBlockEvent? valueOf($core.int value) => _byValue[value];
const BlockEvent._($core.int v, $core.String n) : super(v, n);
const TextBlockEvent._($core.int v, $core.String n) : super(v, n);
}

View File

@ -8,9 +8,9 @@
import 'dart:core' as $core;
import 'dart:convert' as $convert;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use blockEventDescriptor instead')
const BlockEvent$json = const {
'1': 'BlockEvent',
@$core.Deprecated('Use textBlockEventDescriptor instead')
const TextBlockEvent$json = const {
'1': 'TextBlockEvent',
'2': const [
const {'1': 'GetBlockData', '2': 0},
const {'1': 'ApplyDelta', '2': 1},
@ -18,5 +18,5 @@ const BlockEvent$json = const {
],
};
/// Descriptor for `BlockEvent`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List blockEventDescriptor = $convert.base64Decode('CgpCbG9ja0V2ZW50EhAKDEdldEJsb2NrRGF0YRAAEg4KCkFwcGx5RGVsdGEQARISCg5FeHBvcnREb2N1bWVudBAC');
/// Descriptor for `TextBlockEvent`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List textBlockEventDescriptor = $convert.base64Decode('Cg5UZXh0QmxvY2tFdmVudBIQCgxHZXRCbG9ja0RhdGEQABIOCgpBcHBseURlbHRhEAESEgoORXhwb3J0RG9jdW1lbnQQAg==');

View File

@ -72,7 +72,7 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
device_info_plus: ^3.2.1
fluttertoast: ^8.0.8
fluttertoast: ^8.0.9
table_calendar: ^3.0.5
reorderables: ^0.5.0
linked_scroll_controller: ^0.2.0

View File

@ -937,6 +937,7 @@ dependencies = [
"flowy-revision",
"flowy-sync",
"flowy-test",
"futures",
"indexmap",
"lazy_static",
"lib-dispatch",

View File

@ -241,11 +241,11 @@ pub trait ViewDataProcessor {
fn close_container(&self, view_id: &str) -> FutureResult<(), FlowyError>;
fn delta_bytes(&self, view_id: &str) -> FutureResult<Bytes, FlowyError>;
fn view_delta_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError>;
fn create_default_view(&self, user_id: &str, view_id: &str) -> FutureResult<Bytes, FlowyError>;
fn process_create_view_data(&self, user_id: &str, view_id: &str, data: Vec<u8>) -> FutureResult<Bytes, FlowyError>;
fn process_view_delta_data(&self, user_id: &str, view_id: &str, data: Vec<u8>) -> FutureResult<Bytes, FlowyError>;
fn data_type(&self) -> ViewDataType;
}

View File

@ -60,7 +60,7 @@ impl ViewController {
params.data = view_data.to_vec();
} else {
let delta_data = processor
.process_create_view_data(&user_id, &params.view_id, params.data.clone())
.process_view_delta_data(&user_id, &params.view_id, params.data.clone())
.await?;
let _ = self
.create_view(&params.view_id, params.data_type.clone(), delta_data)
@ -176,7 +176,7 @@ impl ViewController {
.await?;
let processor = self.get_data_processor(&view.data_type)?;
let delta_bytes = processor.delta_bytes(view_id).await?;
let delta_bytes = processor.view_delta_data(view_id).await?;
let duplicate_params = CreateViewParams {
belong_to_id: view.belong_to_id.clone(),
name: format!("{} (copy)", &view.name),
@ -238,7 +238,7 @@ impl ViewController {
}
impl ViewController {
#[tracing::instrument(level = "debug", skip(self), err)]
#[tracing::instrument(level = "debug", skip(self, params), err)]
async fn create_view_on_server(&self, params: CreateViewParams) -> Result<View, FlowyError> {
let token = self.user.token()?;
let view = self.cloud_service.create_view(&token, params).await?;

View File

@ -37,6 +37,7 @@ serde_repr = "0.1"
indexmap = {version = "1.8.1", features = ["serde"]}
fancy-regex = "0.10.0"
url = { version = "2"}
futures = "0.3.15"
[dev-dependencies]
flowy-test = { path = "../flowy-test" }

View File

@ -154,11 +154,10 @@ pub async fn make_grid_view_data(
grid_manager: Arc<GridManager>,
build_context: BuildGridContext,
) -> FlowyResult<Bytes> {
let block_id = build_context.block_meta.block_id.clone();
let grid_meta = GridMeta {
grid_id: view_id.to_string(),
fields: build_context.field_metas,
blocks: vec![build_context.block_meta],
blocks: build_context.blocks,
};
// Create grid
@ -168,19 +167,23 @@ pub async fn make_grid_view_data(
Revision::initial_revision(user_id, view_id, grid_delta_data.clone()).into();
let _ = grid_manager.create_grid(view_id, repeated_revision).await?;
for block_meta_data in build_context.blocks_meta_data {
let block_id = block_meta_data.block_id.clone();
// Indexing the block's rows
build_context.block_meta_data.rows.iter().for_each(|row| {
block_meta_data.rows.iter().for_each(|row| {
let _ = grid_manager.block_index_cache.insert(&row.block_id, &row.id);
});
// Create grid's block
let grid_block_meta_delta = make_block_meta_delta(&build_context.block_meta_data);
let grid_block_meta_delta = make_block_meta_delta(&block_meta_data);
let block_meta_delta_data = grid_block_meta_delta.to_delta_bytes();
let repeated_revision: RepeatedRevision =
Revision::initial_revision(user_id, &block_id, block_meta_delta_data).into();
let _ = grid_manager
.create_grid_block_meta(&block_id, repeated_revision)
.await?;
}
Ok(grid_delta_data)
}

View File

@ -0,0 +1,207 @@
// This file is generated by rust-protobuf 2.25.2. Do not edit
// @generated
// https://github.com/rust-lang/rust-clippy/issues/702
#![allow(unknown_lints)]
#![allow(clippy::all)]
#![allow(unused_attributes)]
#![cfg_attr(rustfmt, rustfmt::skip)]
#![allow(box_pointers)]
#![allow(dead_code)]
#![allow(missing_docs)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(trivial_casts)]
#![allow(unused_imports)]
#![allow(unused_results)]
//! Generated file from `format.proto`
/// Generated files are compatible only with the same version
/// of protobuf runtime.
// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2;
#[derive(Clone,PartialEq,Eq,Debug,Hash)]
pub enum NumberFormat {
Number = 0,
USD = 1,
CanadianDollar = 2,
EUR = 4,
Pound = 5,
Yen = 6,
Ruble = 7,
Rupee = 8,
Won = 9,
Yuan = 10,
Real = 11,
Lira = 12,
Rupiah = 13,
Franc = 14,
HongKongDollar = 15,
NewZealandDollar = 16,
Krona = 17,
NorwegianKrone = 18,
MexicanPeso = 19,
Rand = 20,
NewTaiwanDollar = 21,
DanishKrone = 22,
Baht = 23,
Forint = 24,
Koruna = 25,
Shekel = 26,
ChileanPeso = 27,
PhilippinePeso = 28,
Dirham = 29,
ColombianPeso = 30,
Riyal = 31,
Ringgit = 32,
Leu = 33,
ArgentinePeso = 34,
UruguayanPeso = 35,
Percent = 36,
}
impl ::protobuf::ProtobufEnum for NumberFormat {
fn value(&self) -> i32 {
*self as i32
}
fn from_i32(value: i32) -> ::std::option::Option<NumberFormat> {
match value {
0 => ::std::option::Option::Some(NumberFormat::Number),
1 => ::std::option::Option::Some(NumberFormat::USD),
2 => ::std::option::Option::Some(NumberFormat::CanadianDollar),
4 => ::std::option::Option::Some(NumberFormat::EUR),
5 => ::std::option::Option::Some(NumberFormat::Pound),
6 => ::std::option::Option::Some(NumberFormat::Yen),
7 => ::std::option::Option::Some(NumberFormat::Ruble),
8 => ::std::option::Option::Some(NumberFormat::Rupee),
9 => ::std::option::Option::Some(NumberFormat::Won),
10 => ::std::option::Option::Some(NumberFormat::Yuan),
11 => ::std::option::Option::Some(NumberFormat::Real),
12 => ::std::option::Option::Some(NumberFormat::Lira),
13 => ::std::option::Option::Some(NumberFormat::Rupiah),
14 => ::std::option::Option::Some(NumberFormat::Franc),
15 => ::std::option::Option::Some(NumberFormat::HongKongDollar),
16 => ::std::option::Option::Some(NumberFormat::NewZealandDollar),
17 => ::std::option::Option::Some(NumberFormat::Krona),
18 => ::std::option::Option::Some(NumberFormat::NorwegianKrone),
19 => ::std::option::Option::Some(NumberFormat::MexicanPeso),
20 => ::std::option::Option::Some(NumberFormat::Rand),
21 => ::std::option::Option::Some(NumberFormat::NewTaiwanDollar),
22 => ::std::option::Option::Some(NumberFormat::DanishKrone),
23 => ::std::option::Option::Some(NumberFormat::Baht),
24 => ::std::option::Option::Some(NumberFormat::Forint),
25 => ::std::option::Option::Some(NumberFormat::Koruna),
26 => ::std::option::Option::Some(NumberFormat::Shekel),
27 => ::std::option::Option::Some(NumberFormat::ChileanPeso),
28 => ::std::option::Option::Some(NumberFormat::PhilippinePeso),
29 => ::std::option::Option::Some(NumberFormat::Dirham),
30 => ::std::option::Option::Some(NumberFormat::ColombianPeso),
31 => ::std::option::Option::Some(NumberFormat::Riyal),
32 => ::std::option::Option::Some(NumberFormat::Ringgit),
33 => ::std::option::Option::Some(NumberFormat::Leu),
34 => ::std::option::Option::Some(NumberFormat::ArgentinePeso),
35 => ::std::option::Option::Some(NumberFormat::UruguayanPeso),
36 => ::std::option::Option::Some(NumberFormat::Percent),
_ => ::std::option::Option::None
}
}
fn values() -> &'static [Self] {
static values: &'static [NumberFormat] = &[
NumberFormat::Number,
NumberFormat::USD,
NumberFormat::CanadianDollar,
NumberFormat::EUR,
NumberFormat::Pound,
NumberFormat::Yen,
NumberFormat::Ruble,
NumberFormat::Rupee,
NumberFormat::Won,
NumberFormat::Yuan,
NumberFormat::Real,
NumberFormat::Lira,
NumberFormat::Rupiah,
NumberFormat::Franc,
NumberFormat::HongKongDollar,
NumberFormat::NewZealandDollar,
NumberFormat::Krona,
NumberFormat::NorwegianKrone,
NumberFormat::MexicanPeso,
NumberFormat::Rand,
NumberFormat::NewTaiwanDollar,
NumberFormat::DanishKrone,
NumberFormat::Baht,
NumberFormat::Forint,
NumberFormat::Koruna,
NumberFormat::Shekel,
NumberFormat::ChileanPeso,
NumberFormat::PhilippinePeso,
NumberFormat::Dirham,
NumberFormat::ColombianPeso,
NumberFormat::Riyal,
NumberFormat::Ringgit,
NumberFormat::Leu,
NumberFormat::ArgentinePeso,
NumberFormat::UruguayanPeso,
NumberFormat::Percent,
];
values
}
fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::LazyV2::INIT;
descriptor.get(|| {
::protobuf::reflect::EnumDescriptor::new_pb_name::<NumberFormat>("NumberFormat", file_descriptor_proto())
})
}
}
impl ::std::marker::Copy for NumberFormat {
}
impl ::std::default::Default for NumberFormat {
fn default() -> Self {
NumberFormat::Number
}
}
impl ::protobuf::reflect::ProtobufValue for NumberFormat {
fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
::protobuf::reflect::ReflectValueRef::Enum(::protobuf::ProtobufEnum::descriptor(self))
}
}
static file_descriptor_proto_data: &'static [u8] = b"\
\n\x0cformat.proto*\xf8\x03\n\x0cNumberFormat\x12\n\n\x06Number\x10\0\
\x12\x07\n\x03USD\x10\x01\x12\x12\n\x0eCanadianDollar\x10\x02\x12\x07\n\
\x03EUR\x10\x04\x12\t\n\x05Pound\x10\x05\x12\x07\n\x03Yen\x10\x06\x12\t\
\n\x05Ruble\x10\x07\x12\t\n\x05Rupee\x10\x08\x12\x07\n\x03Won\x10\t\x12\
\x08\n\x04Yuan\x10\n\x12\x08\n\x04Real\x10\x0b\x12\x08\n\x04Lira\x10\x0c\
\x12\n\n\x06Rupiah\x10\r\x12\t\n\x05Franc\x10\x0e\x12\x12\n\x0eHongKongD\
ollar\x10\x0f\x12\x14\n\x10NewZealandDollar\x10\x10\x12\t\n\x05Krona\x10\
\x11\x12\x12\n\x0eNorwegianKrone\x10\x12\x12\x0f\n\x0bMexicanPeso\x10\
\x13\x12\x08\n\x04Rand\x10\x14\x12\x13\n\x0fNewTaiwanDollar\x10\x15\x12\
\x0f\n\x0bDanishKrone\x10\x16\x12\x08\n\x04Baht\x10\x17\x12\n\n\x06Forin\
t\x10\x18\x12\n\n\x06Koruna\x10\x19\x12\n\n\x06Shekel\x10\x1a\x12\x0f\n\
\x0bChileanPeso\x10\x1b\x12\x12\n\x0ePhilippinePeso\x10\x1c\x12\n\n\x06D\
irham\x10\x1d\x12\x11\n\rColombianPeso\x10\x1e\x12\t\n\x05Riyal\x10\x1f\
\x12\x0b\n\x07Ringgit\x10\x20\x12\x07\n\x03Leu\x10!\x12\x11\n\rArgentine\
Peso\x10\"\x12\x11\n\rUruguayanPeso\x10#\x12\x0b\n\x07Percent\x10$b\x06p\
roto3\
";
static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;
fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto {
::protobuf::Message::parse_from_bytes(file_descriptor_proto_data).unwrap()
}
pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto {
file_descriptor_proto_lazy.get(|| {
parse_descriptor_proto()
})
}

View File

@ -25,6 +25,9 @@ pub use url_type_option::*;
mod checkbox_type_option;
pub use checkbox_type_option::*;
mod format;
pub use format::*;
mod event_map;
pub use event_map::*;

View File

@ -26,7 +26,7 @@
#[derive(PartialEq,Clone,Default)]
pub struct NumberTypeOption {
// message fields
pub format: NumberFormat,
pub format: super::format::NumberFormat,
pub scale: u32,
pub symbol: ::std::string::String,
pub sign_positive: bool,
@ -50,15 +50,15 @@ impl NumberTypeOption {
// .NumberFormat format = 1;
pub fn get_format(&self) -> NumberFormat {
pub fn get_format(&self) -> super::format::NumberFormat {
self.format
}
pub fn clear_format(&mut self) {
self.format = NumberFormat::Number;
self.format = super::format::NumberFormat::Number;
}
// Param is passed by value, moved
pub fn set_format(&mut self, v: NumberFormat) {
pub fn set_format(&mut self, v: super::format::NumberFormat) {
self.format = v;
}
@ -189,7 +189,7 @@ impl ::protobuf::Message for NumberTypeOption {
#[allow(unused_variables)]
fn compute_size(&self) -> u32 {
let mut my_size = 0;
if self.format != NumberFormat::Number {
if self.format != super::format::NumberFormat::Number {
my_size += ::protobuf::rt::enum_size(1, self.format);
}
if self.scale != 0 {
@ -210,7 +210,7 @@ impl ::protobuf::Message for NumberTypeOption {
}
fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
if self.format != NumberFormat::Number {
if self.format != super::format::NumberFormat::Number {
os.write_enum(1, ::protobuf::ProtobufEnum::value(&self.format))?;
}
if self.scale != 0 {
@ -263,7 +263,7 @@ impl ::protobuf::Message for NumberTypeOption {
static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT;
descriptor.get(|| {
let mut fields = ::std::vec::Vec::new();
fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeEnum<NumberFormat>>(
fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeEnum<super::format::NumberFormat>>(
"format",
|m: &NumberTypeOption| { &m.format },
|m: &mut NumberTypeOption| { &mut m.format },
@ -304,7 +304,7 @@ impl ::protobuf::Message for NumberTypeOption {
impl ::protobuf::Clear for NumberTypeOption {
fn clear(&mut self) {
self.format = NumberFormat::Number;
self.format = super::format::NumberFormat::Number;
self.scale = 0;
self.symbol.clear();
self.sign_positive = false;
@ -325,179 +325,13 @@ impl ::protobuf::reflect::ProtobufValue for NumberTypeOption {
}
}
#[derive(Clone,PartialEq,Eq,Debug,Hash)]
pub enum NumberFormat {
Number = 0,
USD = 1,
CanadianDollar = 2,
EUR = 4,
Pound = 5,
Yen = 6,
Ruble = 7,
Rupee = 8,
Won = 9,
Yuan = 10,
Real = 11,
Lira = 12,
Rupiah = 13,
Franc = 14,
HongKongDollar = 15,
NewZealandDollar = 16,
Krona = 17,
NorwegianKrone = 18,
MexicanPeso = 19,
Rand = 20,
NewTaiwanDollar = 21,
DanishKrone = 22,
Baht = 23,
Forint = 24,
Koruna = 25,
Shekel = 26,
ChileanPeso = 27,
PhilippinePeso = 28,
Dirham = 29,
ColombianPeso = 30,
Riyal = 31,
Ringgit = 32,
Leu = 33,
ArgentinePeso = 34,
UruguayanPeso = 35,
Percent = 36,
}
impl ::protobuf::ProtobufEnum for NumberFormat {
fn value(&self) -> i32 {
*self as i32
}
fn from_i32(value: i32) -> ::std::option::Option<NumberFormat> {
match value {
0 => ::std::option::Option::Some(NumberFormat::Number),
1 => ::std::option::Option::Some(NumberFormat::USD),
2 => ::std::option::Option::Some(NumberFormat::CanadianDollar),
4 => ::std::option::Option::Some(NumberFormat::EUR),
5 => ::std::option::Option::Some(NumberFormat::Pound),
6 => ::std::option::Option::Some(NumberFormat::Yen),
7 => ::std::option::Option::Some(NumberFormat::Ruble),
8 => ::std::option::Option::Some(NumberFormat::Rupee),
9 => ::std::option::Option::Some(NumberFormat::Won),
10 => ::std::option::Option::Some(NumberFormat::Yuan),
11 => ::std::option::Option::Some(NumberFormat::Real),
12 => ::std::option::Option::Some(NumberFormat::Lira),
13 => ::std::option::Option::Some(NumberFormat::Rupiah),
14 => ::std::option::Option::Some(NumberFormat::Franc),
15 => ::std::option::Option::Some(NumberFormat::HongKongDollar),
16 => ::std::option::Option::Some(NumberFormat::NewZealandDollar),
17 => ::std::option::Option::Some(NumberFormat::Krona),
18 => ::std::option::Option::Some(NumberFormat::NorwegianKrone),
19 => ::std::option::Option::Some(NumberFormat::MexicanPeso),
20 => ::std::option::Option::Some(NumberFormat::Rand),
21 => ::std::option::Option::Some(NumberFormat::NewTaiwanDollar),
22 => ::std::option::Option::Some(NumberFormat::DanishKrone),
23 => ::std::option::Option::Some(NumberFormat::Baht),
24 => ::std::option::Option::Some(NumberFormat::Forint),
25 => ::std::option::Option::Some(NumberFormat::Koruna),
26 => ::std::option::Option::Some(NumberFormat::Shekel),
27 => ::std::option::Option::Some(NumberFormat::ChileanPeso),
28 => ::std::option::Option::Some(NumberFormat::PhilippinePeso),
29 => ::std::option::Option::Some(NumberFormat::Dirham),
30 => ::std::option::Option::Some(NumberFormat::ColombianPeso),
31 => ::std::option::Option::Some(NumberFormat::Riyal),
32 => ::std::option::Option::Some(NumberFormat::Ringgit),
33 => ::std::option::Option::Some(NumberFormat::Leu),
34 => ::std::option::Option::Some(NumberFormat::ArgentinePeso),
35 => ::std::option::Option::Some(NumberFormat::UruguayanPeso),
36 => ::std::option::Option::Some(NumberFormat::Percent),
_ => ::std::option::Option::None
}
}
fn values() -> &'static [Self] {
static values: &'static [NumberFormat] = &[
NumberFormat::Number,
NumberFormat::USD,
NumberFormat::CanadianDollar,
NumberFormat::EUR,
NumberFormat::Pound,
NumberFormat::Yen,
NumberFormat::Ruble,
NumberFormat::Rupee,
NumberFormat::Won,
NumberFormat::Yuan,
NumberFormat::Real,
NumberFormat::Lira,
NumberFormat::Rupiah,
NumberFormat::Franc,
NumberFormat::HongKongDollar,
NumberFormat::NewZealandDollar,
NumberFormat::Krona,
NumberFormat::NorwegianKrone,
NumberFormat::MexicanPeso,
NumberFormat::Rand,
NumberFormat::NewTaiwanDollar,
NumberFormat::DanishKrone,
NumberFormat::Baht,
NumberFormat::Forint,
NumberFormat::Koruna,
NumberFormat::Shekel,
NumberFormat::ChileanPeso,
NumberFormat::PhilippinePeso,
NumberFormat::Dirham,
NumberFormat::ColombianPeso,
NumberFormat::Riyal,
NumberFormat::Ringgit,
NumberFormat::Leu,
NumberFormat::ArgentinePeso,
NumberFormat::UruguayanPeso,
NumberFormat::Percent,
];
values
}
fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::LazyV2::INIT;
descriptor.get(|| {
::protobuf::reflect::EnumDescriptor::new_pb_name::<NumberFormat>("NumberFormat", file_descriptor_proto())
})
}
}
impl ::std::marker::Copy for NumberFormat {
}
impl ::std::default::Default for NumberFormat {
fn default() -> Self {
NumberFormat::Number
}
}
impl ::protobuf::reflect::ProtobufValue for NumberFormat {
fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
::protobuf::reflect::ReflectValueRef::Enum(::protobuf::ProtobufEnum::descriptor(self))
}
}
static file_descriptor_proto_data: &'static [u8] = b"\
\n\x18number_type_option.proto\"\xa0\x01\n\x10NumberTypeOption\x12%\n\
\x06format\x18\x01\x20\x01(\x0e2\r.NumberFormatR\x06format\x12\x14\n\x05\
scale\x18\x02\x20\x01(\rR\x05scale\x12\x16\n\x06symbol\x18\x03\x20\x01(\
\tR\x06symbol\x12#\n\rsign_positive\x18\x04\x20\x01(\x08R\x0csignPositiv\
e\x12\x12\n\x04name\x18\x05\x20\x01(\tR\x04name*\xf8\x03\n\x0cNumberForm\
at\x12\n\n\x06Number\x10\0\x12\x07\n\x03USD\x10\x01\x12\x12\n\x0eCanadia\
nDollar\x10\x02\x12\x07\n\x03EUR\x10\x04\x12\t\n\x05Pound\x10\x05\x12\
\x07\n\x03Yen\x10\x06\x12\t\n\x05Ruble\x10\x07\x12\t\n\x05Rupee\x10\x08\
\x12\x07\n\x03Won\x10\t\x12\x08\n\x04Yuan\x10\n\x12\x08\n\x04Real\x10\
\x0b\x12\x08\n\x04Lira\x10\x0c\x12\n\n\x06Rupiah\x10\r\x12\t\n\x05Franc\
\x10\x0e\x12\x12\n\x0eHongKongDollar\x10\x0f\x12\x14\n\x10NewZealandDoll\
ar\x10\x10\x12\t\n\x05Krona\x10\x11\x12\x12\n\x0eNorwegianKrone\x10\x12\
\x12\x0f\n\x0bMexicanPeso\x10\x13\x12\x08\n\x04Rand\x10\x14\x12\x13\n\
\x0fNewTaiwanDollar\x10\x15\x12\x0f\n\x0bDanishKrone\x10\x16\x12\x08\n\
\x04Baht\x10\x17\x12\n\n\x06Forint\x10\x18\x12\n\n\x06Koruna\x10\x19\x12\
\n\n\x06Shekel\x10\x1a\x12\x0f\n\x0bChileanPeso\x10\x1b\x12\x12\n\x0ePhi\
lippinePeso\x10\x1c\x12\n\n\x06Dirham\x10\x1d\x12\x11\n\rColombianPeso\
\x10\x1e\x12\t\n\x05Riyal\x10\x1f\x12\x0b\n\x07Ringgit\x10\x20\x12\x07\n\
\x03Leu\x10!\x12\x11\n\rArgentinePeso\x10\"\x12\x11\n\rUruguayanPeso\x10\
#\x12\x0b\n\x07Percent\x10$b\x06proto3\
\n\x18number_type_option.proto\x1a\x0cformat.proto\"\xa0\x01\n\x10Number\
TypeOption\x12%\n\x06format\x18\x01\x20\x01(\x0e2\r.NumberFormatR\x06for\
mat\x12\x14\n\x05scale\x18\x02\x20\x01(\rR\x05scale\x12\x16\n\x06symbol\
\x18\x03\x20\x01(\tR\x06symbol\x12#\n\rsign_positive\x18\x04\x20\x01(\
\x08R\x0csignPositive\x12\x12\n\x04name\x18\x05\x20\x01(\tR\x04nameb\x06\
proto3\
";
static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

View File

@ -0,0 +1,40 @@
syntax = "proto3";
enum NumberFormat {
Number = 0;
USD = 1;
CanadianDollar = 2;
EUR = 4;
Pound = 5;
Yen = 6;
Ruble = 7;
Rupee = 8;
Won = 9;
Yuan = 10;
Real = 11;
Lira = 12;
Rupiah = 13;
Franc = 14;
HongKongDollar = 15;
NewZealandDollar = 16;
Krona = 17;
NorwegianKrone = 18;
MexicanPeso = 19;
Rand = 20;
NewTaiwanDollar = 21;
DanishKrone = 22;
Baht = 23;
Forint = 24;
Koruna = 25;
Shekel = 26;
ChileanPeso = 27;
PhilippinePeso = 28;
Dirham = 29;
ColombianPeso = 30;
Riyal = 31;
Ringgit = 32;
Leu = 33;
ArgentinePeso = 34;
UruguayanPeso = 35;
Percent = 36;
}

View File

@ -1,4 +1,5 @@
syntax = "proto3";
import "format.proto";
message NumberTypeOption {
NumberFormat format = 1;
@ -7,41 +8,3 @@ message NumberTypeOption {
bool sign_positive = 4;
string name = 5;
}
enum NumberFormat {
Number = 0;
USD = 1;
CanadianDollar = 2;
EUR = 4;
Pound = 5;
Yen = 6;
Ruble = 7;
Rupee = 8;
Won = 9;
Yuan = 10;
Real = 11;
Lira = 12;
Rupiah = 13;
Franc = 14;
HongKongDollar = 15;
NewZealandDollar = 16;
Krona = 17;
NorwegianKrone = 18;
MexicanPeso = 19;
Rand = 20;
NewTaiwanDollar = 21;
DanishKrone = 22;
Baht = 23;
Forint = 24;
Koruna = 25;
Shekel = 26;
ChileanPeso = 27;
PhilippinePeso = 28;
Dirham = 29;
ColombianPeso = 30;
Riyal = 31;
Ringgit = 32;
Leu = 33;
ArgentinePeso = 34;
UruguayanPeso = 35;
Percent = 36;
}

View File

@ -1,6 +1,6 @@
use bytes::Bytes;
use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::entities::{CellMeta, RowMeta, RowMetaChangeset, RowOrder};
use flowy_grid_data_model::entities::{CellMeta, GridBlockMetaData, RowMeta, RowMetaChangeset, RowOrder};
use flowy_revision::{RevisionCloudService, RevisionCompactor, RevisionManager, RevisionObjectBuilder};
use flowy_sync::client_grid::{GridBlockMetaChange, GridBlockMetaPad};
use flowy_sync::entities::revision::Revision;
@ -41,6 +41,10 @@ impl GridBlockMetaEditor {
})
}
pub async fn duplicate_block_meta_data(&self, duplicated_block_id: &str) -> GridBlockMetaData {
self.pad.read().await.duplicate_data(duplicated_block_id).await
}
/// return current number of rows and the inserted index. The inserted index will be None if the start_row_id is None
pub(crate) async fn create_row(
&self,

View File

@ -47,7 +47,7 @@ impl GridBlockManager {
debug_assert!(!block_id.is_empty());
match self.block_editor_map.get(block_id) {
None => {
tracing::error!("The is a fatal error, block is not exist");
tracing::error!("This is a fatal error, block with id:{} is not exist", block_id);
let editor = Arc::new(make_block_meta_editor(&self.user, block_id).await?);
self.block_editor_map.insert(block_id.to_owned(), editor.clone());
Ok(editor)
@ -267,6 +267,7 @@ async fn make_block_meta_editor_map(
}
async fn make_block_meta_editor(user: &Arc<dyn GridUser>, block_id: &str) -> FlowyResult<GridBlockMetaEditor> {
tracing::trace!("Open block:{} meta editor", block_id);
let token = user.token()?;
let user_id = user.user_id()?;
let pool = user.db_pool()?;

View File

@ -42,7 +42,7 @@ impl_type_option!(CheckboxTypeOption, FieldType::Checkbox);
const YES: &str = "Yes";
const NO: &str = "No";
impl CellDataOperation<String, String> for CheckboxTypeOption {
impl CellDataOperation<String> for CheckboxTypeOption {
fn decode_cell_data<T>(
&self,
encoded_data: T,

View File

@ -1,17 +1,16 @@
use crate::entities::{CellIdentifier, CellIdentifierPayload};
use crate::impl_type_option;
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
use crate::services::row::{CellContentChangeset, CellDataOperation, DecodedCellData, EncodedCellData};
use crate::services::row::{CellContentChangeset, CellDataOperation, DecodedCellData};
use bytes::Bytes;
use chrono::format::strftime::StrftimeItems;
use chrono::NaiveDateTime;
use chrono::{NaiveDateTime, Timelike};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult};
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use flowy_grid_data_model::entities::{
CellChangeset, CellMeta, FieldMeta, FieldType, TypeOptionDataDeserializer, TypeOptionDataEntry,
};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use strum_macros::EnumIter;
// Date
@ -29,35 +28,36 @@ pub struct DateTypeOption {
impl_type_option!(DateTypeOption, FieldType::DateTime);
impl DateTypeOption {
fn today_desc_from_timestamp(&self, timestamp: i64, time: &Option<String>) -> String {
let native = chrono::NaiveDateTime::from_timestamp(timestamp, 0);
self.today_desc_from_native(native, time)
}
#[allow(dead_code)]
fn today_desc_from_str(&self, s: String, time: &Option<String>) -> String {
match NaiveDateTime::parse_from_str(&s, &self.date_fmt(time)) {
Ok(native) => self.today_desc_from_native(native, time),
Err(_) => "".to_owned(),
}
pub fn new() -> Self {
Self::default()
}
fn today_desc_from_native(&self, native: chrono::NaiveDateTime, time: &Option<String>) -> String {
fn today_desc_from_timestamp(&self, timestamp: i64) -> DateCellData {
let native = chrono::NaiveDateTime::from_timestamp(timestamp, 0);
self.date_from_native(native)
}
fn date_from_native(&self, native: chrono::NaiveDateTime) -> DateCellData {
if native.timestamp() == 0 {
return DateCellData::default();
}
let time = native.time();
let has_time = time.hour() != 0 || time.second() != 0;
let utc = self.utc_date_time_from_native(native);
// let china_timezone = FixedOffset::east(8 * 3600);
// let a = utc.with_timezone(&china_timezone);
let fmt = self.date_fmt(time);
let output = format!("{}", utc.format_with_items(StrftimeItems::new(&fmt)));
output
let fmt = self.date_format.format_str();
let date = format!("{}", utc.format_with_items(StrftimeItems::new(fmt)));
let mut time = "".to_string();
if has_time {
let fmt = format!("{} {}", self.date_format.format_str(), self.time_format.format_str());
time = format!("{}", utc.format_with_items(StrftimeItems::new(&fmt))).replace(&date, "");
}
fn utc_date_time_from_timestamp(&self, timestamp: i64) -> chrono::DateTime<chrono::Utc> {
let native = NaiveDateTime::from_timestamp(timestamp, 0);
self.utc_date_time_from_native(native)
}
fn utc_date_time_from_native(&self, naive: chrono::NaiveDateTime) -> chrono::DateTime<chrono::Utc> {
chrono::DateTime::<chrono::Utc>::from_utc(naive, chrono::Utc)
let timestamp = native.timestamp();
DateCellData { date, time, timestamp }
}
fn date_fmt(&self, time: &Option<String>) -> String {
@ -77,14 +77,6 @@ impl DateTypeOption {
}
}
fn date_desc_from_timestamp(&self, serde_cell_data: &DateCellDataSerde) -> String {
if serde_cell_data.timestamp == 0 {
return "".to_owned();
}
self.today_desc_from_timestamp(serde_cell_data.timestamp, &serde_cell_data.time)
}
fn timestamp_from_utc_with_time(
&self,
utc: &chrono::DateTime<chrono::Utc>,
@ -113,9 +105,18 @@ impl DateTypeOption {
Ok(utc.timestamp())
}
fn utc_date_time_from_timestamp(&self, timestamp: i64) -> chrono::DateTime<chrono::Utc> {
let native = NaiveDateTime::from_timestamp(timestamp, 0);
self.utc_date_time_from_native(native)
}
impl CellDataOperation<EncodedCellData<DateCellDataSerde>, DateCellDataSerde> for DateTypeOption {
fn utc_date_time_from_native(&self, naive: chrono::NaiveDateTime) -> chrono::DateTime<chrono::Utc> {
chrono::DateTime::<chrono::Utc>::from_utc(naive, chrono::Utc)
}
}
impl CellDataOperation<String> for DateTypeOption {
fn decode_cell_data<T>(
&self,
encoded_data: T,
@ -123,7 +124,7 @@ impl CellDataOperation<EncodedCellData<DateCellDataSerde>, DateCellDataSerde> fo
_field_meta: &FieldMeta,
) -> FlowyResult<DecodedCellData>
where
T: Into<EncodedCellData<DateCellDataSerde>>,
T: Into<String>,
{
// Return default data if the type_option_cell_data is not FieldType::DateTime.
// It happens when switching from one field to another.
@ -133,33 +134,29 @@ impl CellDataOperation<EncodedCellData<DateCellDataSerde>, DateCellDataSerde> fo
return Ok(DecodedCellData::default());
}
let encoded_data = encoded_data.into().try_into_inner()?;
let date = self.date_desc_from_timestamp(&encoded_data);
let time = encoded_data.time.unwrap_or_else(|| "".to_owned());
let timestamp = encoded_data.timestamp;
DecodedCellData::try_from_bytes(DateCellData { date, time, timestamp })
let timestamp = encoded_data.into().parse::<i64>().unwrap_or(0);
let date = self.today_desc_from_timestamp(timestamp);
DecodedCellData::try_from_bytes(date)
}
fn apply_changeset<C>(&self, changeset: C, _cell_meta: Option<CellMeta>) -> Result<DateCellDataSerde, FlowyError>
fn apply_changeset<C>(&self, changeset: C, _cell_meta: Option<CellMeta>) -> Result<String, FlowyError>
where
C: Into<CellContentChangeset>,
{
let content_changeset: DateCellContentChangeset = serde_json::from_str(&changeset.into())?;
let cell_data = match content_changeset.date_timestamp() {
None => DateCellDataSerde::default(),
None => 0,
Some(date_timestamp) => match (self.include_time, content_changeset.time) {
(true, Some(time)) => {
let time = Some(time.trim().to_uppercase());
let utc = self.utc_date_time_from_timestamp(date_timestamp);
let timestamp = self.timestamp_from_utc_with_time(&utc, &time)?;
DateCellDataSerde::new(timestamp, time, &self.time_format)
self.timestamp_from_utc_with_time(&utc, &time)?
}
_ => DateCellDataSerde::from_timestamp(date_timestamp, Some(default_time_str(&self.time_format))),
_ => date_timestamp,
},
};
Ok(cell_data)
Ok(cell_data.to_string())
}
}
@ -283,46 +280,6 @@ pub struct DateCellData {
pub timestamp: i64,
}
#[derive(Default, Serialize, Deserialize)]
pub struct DateCellDataSerde {
pub timestamp: i64,
pub time: Option<String>,
}
impl DateCellDataSerde {
fn new(timestamp: i64, time: Option<String>, time_format: &TimeFormat) -> Self {
Self {
timestamp,
time: Some(time.unwrap_or_else(|| default_time_str(time_format))),
}
}
pub(crate) fn from_timestamp(timestamp: i64, time: Option<String>) -> Self {
Self { timestamp, time }
}
}
impl FromStr for DateCellDataSerde {
type Err = FlowyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str::<DateCellDataSerde>(s).map_err(internal_error)
}
}
impl ToString for DateCellDataSerde {
fn to_string(&self) -> String {
serde_json::to_string(&self).unwrap_or_else(|_| "".to_string())
}
}
fn default_time_str(time_format: &TimeFormat) -> String {
match time_format {
TimeFormat::TwelveHour => "12:00 AM".to_string(),
TimeFormat::TwentyFourHour => "00:00".to_string(),
}
}
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct DateChangesetPayload {
#[pb(index = 1)]
@ -399,15 +356,13 @@ impl std::convert::From<DateCellContentChangeset> for CellContentChangeset {
#[cfg(test)]
mod tests {
use crate::services::field::FieldBuilder;
use crate::services::field::{
DateCellContentChangeset, DateCellData, DateCellDataSerde, DateFormat, DateTypeOption, TimeFormat,
};
use crate::services::row::{CellDataOperation, EncodedCellData};
use flowy_grid_data_model::entities::{FieldMeta, FieldType};
use crate::services::field::{DateCellContentChangeset, DateCellData, DateFormat, DateTypeOption, TimeFormat};
use crate::services::row::CellDataOperation;
use flowy_grid_data_model::entities::{FieldMeta, FieldType, TypeOptionDataEntry};
use strum::IntoEnumIterator;
#[test]
fn date_description_invalid_input_test() {
fn date_type_option_invalid_input_test() {
let type_option = DateTypeOption::default();
let field_type = FieldType::DateTime;
let field_meta = FieldBuilder::from_field_type(&field_type).build();
@ -424,7 +379,7 @@ mod tests {
}
#[test]
fn date_description_date_format_test() {
fn date_type_option_date_format_test() {
let mut type_option = DateTypeOption::default();
let field_meta = FieldBuilder::from_field_type(&FieldType::DateTime).build();
for date_format in DateFormat::iter() {
@ -447,7 +402,7 @@ mod tests {
}
#[test]
fn date_description_time_format_test() {
fn date_type_option_time_format_test() {
let mut type_option = DateTypeOption::default();
let field_type = FieldType::DateTime;
let field_meta = FieldBuilder::from_field_type(&field_type).build();
@ -465,7 +420,7 @@ mod tests {
},
&field_type,
&field_meta,
"May 27,2022 00:00",
"May 27,2022",
);
assert_changeset_result(
&type_option,
@ -487,9 +442,9 @@ mod tests {
},
&field_type,
&field_meta,
"May 27,2022 12:00 AM",
"May 27,2022",
);
//
assert_changeset_result(
&type_option,
DateCellContentChangeset {
@ -517,8 +472,8 @@ mod tests {
}
#[test]
fn date_description_apply_changeset_test() {
let mut type_option = DateTypeOption::default();
fn date_type_option_apply_changeset_test() {
let mut type_option = DateTypeOption::new();
let field_type = FieldType::DateTime;
let field_meta = FieldBuilder::from_field_type(&field_type).build();
let date_timestamp = "1653609600".to_owned();
@ -543,7 +498,7 @@ mod tests {
},
&field_type,
&field_meta,
"May 27,2022 00:00",
"May 27,2022",
);
assert_changeset_result(
@ -572,30 +527,53 @@ mod tests {
#[test]
#[should_panic]
fn date_description_apply_changeset_error_test() {
let mut type_option = DateTypeOption::default();
fn date_type_option_apply_changeset_error_test() {
let mut type_option = DateTypeOption::new();
type_option.include_time = true;
let _field_meta = FieldBuilder::from_field_type(&FieldType::DateTime).build();
let field_meta = FieldBuilder::from_field_type(&type_option.field_type()).build();
let date_timestamp = "1653609600".to_owned();
let changeset = DateCellContentChangeset {
assert_changeset_result(
&type_option,
DateCellContentChangeset {
date: Some(date_timestamp.clone()),
time: Some("1:a0".to_owned()),
};
let _ = type_option.apply_changeset(changeset, None).unwrap();
let changeset = DateCellContentChangeset {
date: Some(date_timestamp),
time: Some("1:".to_owned()),
};
let _ = type_option.apply_changeset(changeset, None).unwrap();
},
&type_option.field_type(),
&field_meta,
"May 27,2022 01:00",
);
assert_changeset_result(
&type_option,
DateCellContentChangeset {
date: Some(date_timestamp),
time: Some("1:00".to_owned()),
},
&type_option.field_type(),
&field_meta,
"May 27,2022 01:00",
);
}
#[test]
#[should_panic]
fn date_description_invalid_data_test() {
let type_option = DateTypeOption::default();
type_option.apply_changeset("he", None).unwrap();
fn date_type_option_twelve_hours_to_twenty_four_hours() {
let mut type_option = DateTypeOption::new();
type_option.include_time = true;
let field_meta = FieldBuilder::from_field_type(&type_option.field_type()).build();
let date_timestamp = "1653609600".to_owned();
assert_changeset_result(
&type_option,
DateCellContentChangeset {
date: Some(date_timestamp),
time: Some("1:00 am".to_owned()),
},
&type_option.field_type(),
&field_meta,
"May 27,2022 01:00",
);
}
fn assert_changeset_result(
@ -605,7 +583,7 @@ mod tests {
field_meta: &FieldMeta,
expected: &str,
) {
let encoded_data = EncodedCellData(Some(type_option.apply_changeset(changeset, None).unwrap()));
let encoded_data = type_option.apply_changeset(changeset, None).unwrap();
assert_eq!(
expected.to_owned(),
decode_cell_data(encoded_data, type_option, field_meta)
@ -613,24 +591,37 @@ mod tests {
}
fn assert_decode_timestamp(timestamp: i64, type_option: &DateTypeOption, field_meta: &FieldMeta, expected: &str) {
let serde_json = DateCellDataSerde { timestamp, time: None }.to_string();
let encoded_data = type_option
.apply_changeset(
DateCellContentChangeset {
date: Some(timestamp.to_string()),
time: None,
},
None,
)
.unwrap();
assert_eq!(
expected.to_owned(),
decode_cell_data(serde_json, type_option, field_meta)
decode_cell_data(encoded_data, type_option, field_meta)
);
}
fn decode_cell_data<T: Into<EncodedCellData<DateCellDataSerde>>>(
fn decode_cell_data<T: Into<String>>(
encoded_data: T,
type_option: &DateTypeOption,
field_meta: &FieldMeta,
) -> String {
type_option
let decoded_data = type_option
.decode_cell_data(encoded_data, &FieldType::DateTime, field_meta)
.unwrap()
.parse::<DateCellData>()
.unwrap()
.date
.unwrap();
if type_option.include_time {
format!("{}{}", decoded_data.date, decoded_data.time)
} else {
decoded_data.date
}
}
}

View File

@ -1,182 +1,16 @@
use crate::impl_type_option;
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
use crate::services::row::{CellContentChangeset, CellDataOperation, DecodedCellData};
use bytes::Bytes;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::entities::{
CellMeta, FieldMeta, FieldType, TypeOptionDataDeserializer, TypeOptionDataEntry,
};
use flowy_derive::ProtoBuf_Enum;
use lazy_static::lazy_static;
use rust_decimal::Decimal;
use rusty_money::define_currency_set;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
lazy_static! {
static ref STRIP_SYMBOL: Vec<String> = make_strip_symbol();
}
#[derive(Default)]
pub struct NumberTypeOptionBuilder(NumberTypeOption);
impl_into_box_type_option_builder!(NumberTypeOptionBuilder);
impl_builder_from_json_str_and_from_bytes!(NumberTypeOptionBuilder, NumberTypeOption);
impl NumberTypeOptionBuilder {
pub fn name(mut self, name: &str) -> Self {
self.0.name = name.to_string();
self
}
pub fn set_format(mut self, format: NumberFormat) -> Self {
self.0.set_format(format);
self
}
pub fn scale(mut self, scale: u32) -> Self {
self.0.scale = scale;
self
}
pub fn positive(mut self, positive: bool) -> Self {
self.0.sign_positive = positive;
self
}
}
impl TypeOptionBuilder for NumberTypeOptionBuilder {
fn field_type(&self) -> FieldType {
self.0.field_type()
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
&self.0
}
}
// Number
#[derive(Clone, Debug, Serialize, Deserialize, ProtoBuf)]
pub struct NumberTypeOption {
#[pb(index = 1)]
pub format: NumberFormat,
#[pb(index = 2)]
pub scale: u32,
#[pb(index = 3)]
pub symbol: String,
#[pb(index = 4)]
pub sign_positive: bool,
#[pb(index = 5)]
pub name: String,
}
impl_type_option!(NumberTypeOption, FieldType::Number);
impl CellDataOperation<String, String> for NumberTypeOption {
fn decode_cell_data<T>(
&self,
encoded_data: T,
decoded_field_type: &FieldType,
_field_meta: &FieldMeta,
) -> FlowyResult<DecodedCellData>
where
T: Into<String>,
{
if decoded_field_type.is_date() {
return Ok(DecodedCellData::default());
}
let cell_data = encoded_data.into();
match self.format {
NumberFormat::Number => {
if let Ok(v) = cell_data.parse::<f64>() {
return Ok(DecodedCellData::new(v.to_string()));
}
if let Ok(v) = cell_data.parse::<i64>() {
return Ok(DecodedCellData::new(v.to_string()));
}
Ok(DecodedCellData::default())
}
NumberFormat::Percent => {
let content = cell_data.parse::<f64>().map_or(String::new(), |v| v.to_string());
Ok(DecodedCellData::new(content))
}
_ => {
let content = self.money_from_str(&cell_data);
Ok(DecodedCellData::new(content))
}
}
}
fn apply_changeset<C>(&self, changeset: C, _cell_meta: Option<CellMeta>) -> Result<String, FlowyError>
where
C: Into<CellContentChangeset>,
{
let changeset = changeset.into();
let mut data = changeset.trim().to_string();
if self.format != NumberFormat::Number {
data = self.strip_symbol(data);
if !data.chars().all(char::is_numeric) {
return Err(FlowyError::invalid_data().context("Should only contain numbers"));
}
}
Ok(data)
}
}
impl std::default::Default for NumberTypeOption {
fn default() -> Self {
let format = NumberFormat::default();
let symbol = format.symbol();
NumberTypeOption {
format,
scale: 0,
symbol,
sign_positive: true,
name: "Number".to_string(),
}
}
}
impl NumberTypeOption {
pub fn set_format(&mut self, format: NumberFormat) {
self.format = format;
self.symbol = format.symbol();
}
fn money_from_str(&self, s: &str) -> String {
match Decimal::from_str(s) {
Ok(mut decimal) => {
match decimal.set_scale(self.scale) {
Ok(_) => {}
Err(e) => {
tracing::error!("Set decimal scale failed: {:?}", e);
}
}
decimal.set_sign_positive(self.sign_positive);
let money = rusty_money::Money::from_decimal(decimal, self.format.currency());
money.to_string()
}
Err(_) => String::new(),
}
}
fn strip_symbol<T: ToString>(&self, s: T) -> String {
let mut s = s.to_string();
if !s.chars().all(char::is_numeric) {
s.retain(|c| !STRIP_SYMBOL.contains(&c.to_string()));
}
s
}
pub static ref CURRENCY_SYMBOL: Vec<String> = NumberFormat::iter()
.map(|format| format.symbol())
.collect::<Vec<String>>();
pub static ref STRIP_SYMBOL: Vec<String> = vec![",".to_owned(), ".".to_owned()];
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter, Serialize, Deserialize, ProtoBuf_Enum)]
@ -609,137 +443,3 @@ impl NumberFormat {
self.currency().symbol.to_string()
}
}
fn make_strip_symbol() -> Vec<String> {
let mut symbols = vec![",".to_owned(), ".".to_owned()];
for format in NumberFormat::iter() {
symbols.push(format.symbol());
}
symbols
}
#[cfg(test)]
mod tests {
use crate::services::field::FieldBuilder;
use crate::services::field::{NumberFormat, NumberTypeOption};
use crate::services::row::CellDataOperation;
use flowy_grid_data_model::entities::{FieldMeta, FieldType};
use strum::IntoEnumIterator;
#[test]
fn number_description_invalid_input_test() {
let type_option = NumberTypeOption::default();
let field_type = FieldType::Number;
let field_meta = FieldBuilder::from_field_type(&field_type).build();
assert_equal(&type_option, "", "", &field_type, &field_meta);
assert_equal(&type_option, "abc", "", &field_type, &field_meta);
}
#[test]
fn number_description_test() {
let mut type_option = NumberTypeOption::default();
let field_type = FieldType::Number;
let field_meta = FieldBuilder::from_field_type(&field_type).build();
assert_eq!(type_option.strip_symbol("¥18,443"), "18443".to_owned());
assert_eq!(type_option.strip_symbol("$18,443"), "18443".to_owned());
assert_eq!(type_option.strip_symbol("€18.443"), "18443".to_owned());
for format in NumberFormat::iter() {
type_option.format = format;
match format {
NumberFormat::Number => {
assert_equal(&type_option, "18443", "18443", &field_type, &field_meta);
}
NumberFormat::USD => {
assert_equal(&type_option, "18443", "$18,443", &field_type, &field_meta);
assert_equal(&type_option, "", "", &field_type, &field_meta);
assert_equal(&type_option, "abc", "", &field_type, &field_meta);
}
NumberFormat::Yen => {
assert_equal(&type_option, "18443", "¥18,443", &field_type, &field_meta);
}
NumberFormat::Yuan => {
assert_equal(&type_option, "18443", "CN¥18,443", &field_type, &field_meta);
}
NumberFormat::EUR => {
assert_equal(&type_option, "18443", "€18.443", &field_type, &field_meta);
}
_ => {}
}
}
}
#[test]
fn number_description_scale_test() {
let mut type_option = NumberTypeOption {
scale: 1,
..Default::default()
};
let field_type = FieldType::Number;
let field_meta = FieldBuilder::from_field_type(&field_type).build();
for format in NumberFormat::iter() {
type_option.format = format;
match format {
NumberFormat::Number => {
assert_equal(&type_option, "18443", "18443", &field_type, &field_meta);
}
NumberFormat::USD => {
assert_equal(&type_option, "18443", "$1,844.3", &field_type, &field_meta);
}
NumberFormat::Yen => {
assert_equal(&type_option, "18443", "¥1,844.3", &field_type, &field_meta);
}
NumberFormat::EUR => {
assert_equal(&type_option, "18443", "€1.844,3", &field_type, &field_meta);
}
_ => {}
}
}
}
#[test]
fn number_description_sign_test() {
let mut type_option = NumberTypeOption {
sign_positive: false,
..Default::default()
};
let field_type = FieldType::Number;
let field_meta = FieldBuilder::from_field_type(&field_type).build();
for format in NumberFormat::iter() {
type_option.format = format;
match format {
NumberFormat::Number => {
assert_equal(&type_option, "18443", "18443", &field_type, &field_meta);
}
NumberFormat::USD => {
assert_equal(&type_option, "18443", "-$18,443", &field_type, &field_meta);
}
NumberFormat::Yen => {
assert_equal(&type_option, "18443", "-¥18,443", &field_type, &field_meta);
}
NumberFormat::EUR => {
assert_equal(&type_option, "18443", "-€18.443", &field_type, &field_meta);
}
_ => {}
}
}
}
fn assert_equal(
type_option: &NumberTypeOption,
cell_data: &str,
expected_str: &str,
field_type: &FieldType,
field_meta: &FieldMeta,
) {
assert_eq!(
type_option
.decode_cell_data(cell_data, field_type, field_meta)
.unwrap()
.to_string(),
expected_str.to_owned()
);
}
}

View File

@ -0,0 +1,6 @@
#![allow(clippy::module_inception)]
mod format;
mod number_type_option;
pub use format::*;
pub use number_type_option::*;

View File

@ -0,0 +1,346 @@
use crate::impl_type_option;
use crate::services::field::type_options::number_type_option::format::*;
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
use crate::services::row::{CellContentChangeset, CellDataOperation, DecodedCellData};
use bytes::Bytes;
use flowy_derive::ProtoBuf;
use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::entities::{
CellMeta, FieldMeta, FieldType, TypeOptionDataDeserializer, TypeOptionDataEntry,
};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Default)]
pub struct NumberTypeOptionBuilder(NumberTypeOption);
impl_into_box_type_option_builder!(NumberTypeOptionBuilder);
impl_builder_from_json_str_and_from_bytes!(NumberTypeOptionBuilder, NumberTypeOption);
impl NumberTypeOptionBuilder {
pub fn name(mut self, name: &str) -> Self {
self.0.name = name.to_string();
self
}
pub fn set_format(mut self, format: NumberFormat) -> Self {
self.0.set_format(format);
self
}
pub fn scale(mut self, scale: u32) -> Self {
self.0.scale = scale;
self
}
pub fn positive(mut self, positive: bool) -> Self {
self.0.sign_positive = positive;
self
}
}
impl TypeOptionBuilder for NumberTypeOptionBuilder {
fn field_type(&self) -> FieldType {
self.0.field_type()
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
&self.0
}
}
// Number
#[derive(Clone, Debug, Serialize, Deserialize, ProtoBuf)]
pub struct NumberTypeOption {
#[pb(index = 1)]
pub format: NumberFormat,
#[pb(index = 2)]
pub scale: u32,
#[pb(index = 3)]
pub symbol: String,
#[pb(index = 4)]
pub sign_positive: bool,
#[pb(index = 5)]
pub name: String,
}
impl_type_option!(NumberTypeOption, FieldType::Number);
impl NumberTypeOption {
pub fn new() -> Self {
Self::default()
}
fn cell_content_from_number_str(&self, s: &str) -> FlowyResult<String> {
match self.format {
NumberFormat::Number => {
if let Ok(v) = s.parse::<f64>() {
return Ok(v.to_string());
}
if let Ok(v) = s.parse::<i64>() {
return Ok(v.to_string());
}
Ok("".to_string())
}
NumberFormat::Percent => {
let content = s.parse::<f64>().map_or(String::new(), |v| v.to_string());
Ok(content)
}
_ => self.money_from_number_str(s),
}
}
pub fn set_format(&mut self, format: NumberFormat) {
self.format = format;
self.symbol = format.symbol();
}
fn money_from_number_str(&self, s: &str) -> FlowyResult<String> {
let mut number = self.strip_currency_symbol(s);
if s.is_empty() {
return Ok("".to_string());
}
match Decimal::from_str(&number) {
Ok(mut decimal) => {
decimal.set_sign_positive(self.sign_positive);
let money = rusty_money::Money::from_decimal(decimal, self.format.currency()).to_string();
Ok(money)
}
Err(_) => match rusty_money::Money::from_str(&number, self.format.currency()) {
Ok(money) => Ok(money.to_string()),
Err(_) => {
number.retain(|c| !STRIP_SYMBOL.contains(&c.to_string()));
if number.chars().all(char::is_numeric) {
self.money_from_number_str(&number)
} else {
Err(FlowyError::invalid_data().context("Should only contain numbers"))
}
}
},
}
}
fn strip_currency_symbol<T: ToString>(&self, s: T) -> String {
let mut s = s.to_string();
for symbol in CURRENCY_SYMBOL.iter() {
if s.starts_with(symbol) {
s = s.strip_prefix(symbol).unwrap_or("").to_string();
break;
}
}
s
}
}
impl CellDataOperation<String> for NumberTypeOption {
fn decode_cell_data<T>(
&self,
encoded_data: T,
decoded_field_type: &FieldType,
_field_meta: &FieldMeta,
) -> FlowyResult<DecodedCellData>
where
T: Into<String>,
{
if decoded_field_type.is_date() {
return Ok(DecodedCellData::default());
}
let cell_data = encoded_data.into();
match self.format {
NumberFormat::Number => {
if let Ok(v) = cell_data.parse::<f64>() {
return Ok(DecodedCellData::new(v.to_string()));
}
if let Ok(v) = cell_data.parse::<i64>() {
return Ok(DecodedCellData::new(v.to_string()));
}
Ok(DecodedCellData::default())
}
NumberFormat::Percent => {
let content = cell_data.parse::<f64>().map_or(String::new(), |v| v.to_string());
Ok(DecodedCellData::new(content))
}
_ => {
let content = self
.money_from_number_str(&cell_data)
.unwrap_or_else(|_| "".to_string());
Ok(DecodedCellData::new(content))
}
}
}
fn apply_changeset<C>(&self, changeset: C, _cell_meta: Option<CellMeta>) -> Result<String, FlowyError>
where
C: Into<CellContentChangeset>,
{
let changeset = changeset.into();
let data = changeset.trim().to_string();
let _ = self.cell_content_from_number_str(&data)?;
Ok(data)
}
}
impl std::default::Default for NumberTypeOption {
fn default() -> Self {
let format = NumberFormat::default();
let symbol = format.symbol();
NumberTypeOption {
format,
scale: 0,
symbol,
sign_positive: true,
name: "Number".to_string(),
}
}
}
#[cfg(test)]
mod tests {
use crate::services::field::FieldBuilder;
use crate::services::field::{NumberFormat, NumberTypeOption};
use crate::services::row::CellDataOperation;
use flowy_grid_data_model::entities::{FieldMeta, FieldType};
use strum::IntoEnumIterator;
#[test]
fn number_type_option_invalid_input_test() {
let type_option = NumberTypeOption::default();
let field_type = FieldType::Number;
let field_meta = FieldBuilder::from_field_type(&field_type).build();
assert_equal(&type_option, "", "", &field_type, &field_meta);
assert_equal(&type_option, "abc", "", &field_type, &field_meta);
}
#[test]
fn number_type_option_strip_symbol_test() {
let mut type_option = NumberTypeOption::new();
type_option.format = NumberFormat::USD;
assert_eq!(type_option.strip_currency_symbol("$18,443"), "18,443".to_owned());
type_option.format = NumberFormat::Yuan;
assert_eq!(type_option.strip_currency_symbol("$0.2"), "0.2".to_owned());
}
#[test]
fn number_type_option_format_number_test() {
let mut type_option = NumberTypeOption::default();
let field_type = FieldType::Number;
let field_meta = FieldBuilder::from_field_type(&field_type).build();
for format in NumberFormat::iter() {
type_option.format = format;
match format {
NumberFormat::Number => {
assert_equal(&type_option, "18443", "18443", &field_type, &field_meta);
}
NumberFormat::USD => {
assert_equal(&type_option, "18443", "$18,443", &field_type, &field_meta);
}
NumberFormat::Yen => {
assert_equal(&type_option, "18443", "¥18,443", &field_type, &field_meta);
}
NumberFormat::Yuan => {
assert_equal(&type_option, "18443", "CN¥18,443", &field_type, &field_meta);
}
NumberFormat::EUR => {
assert_equal(&type_option, "18443", "€18.443", &field_type, &field_meta);
}
_ => {}
}
}
}
#[test]
fn number_type_option_format_str_test() {
let mut type_option = NumberTypeOption::default();
let field_type = FieldType::Number;
let field_meta = FieldBuilder::from_field_type(&field_type).build();
for format in NumberFormat::iter() {
type_option.format = format;
match format {
NumberFormat::Number => {
assert_equal(&type_option, "18443", "18443", &field_type, &field_meta);
assert_equal(&type_option, "0.2", "0.2", &field_type, &field_meta);
}
NumberFormat::USD => {
assert_equal(&type_option, "$18,44", "$1,844", &field_type, &field_meta);
assert_equal(&type_option, "$0.2", "$0.2", &field_type, &field_meta);
assert_equal(&type_option, "", "", &field_type, &field_meta);
assert_equal(&type_option, "abc", "", &field_type, &field_meta);
}
NumberFormat::Yen => {
assert_equal(&type_option, "¥18,44", "¥1,844", &field_type, &field_meta);
assert_equal(&type_option, "¥1844", "¥1,844", &field_type, &field_meta);
}
NumberFormat::Yuan => {
assert_equal(&type_option, "CN¥18,44", "CN¥1,844", &field_type, &field_meta);
assert_equal(&type_option, "CN¥1844", "CN¥1,844", &field_type, &field_meta);
}
NumberFormat::EUR => {
assert_equal(&type_option, "€18.44", "€18,44", &field_type, &field_meta);
assert_equal(&type_option, "€0.5", "€0,5", &field_type, &field_meta);
assert_equal(&type_option, "€1844", "€1.844", &field_type, &field_meta);
}
_ => {}
}
}
}
#[test]
fn number_description_sign_test() {
let mut type_option = NumberTypeOption {
sign_positive: false,
..Default::default()
};
let field_type = FieldType::Number;
let field_meta = FieldBuilder::from_field_type(&field_type).build();
for format in NumberFormat::iter() {
type_option.format = format;
match format {
NumberFormat::Number => {
assert_equal(&type_option, "18443", "18443", &field_type, &field_meta);
}
NumberFormat::USD => {
assert_equal(&type_option, "18443", "-$18,443", &field_type, &field_meta);
}
NumberFormat::Yen => {
assert_equal(&type_option, "18443", "-¥18,443", &field_type, &field_meta);
}
NumberFormat::EUR => {
assert_equal(&type_option, "18443", "-€18.443", &field_type, &field_meta);
}
_ => {}
}
}
}
fn assert_equal(
type_option: &NumberTypeOption,
cell_data: &str,
expected_str: &str,
field_type: &FieldType,
field_meta: &FieldMeta,
) {
assert_eq!(
type_option
.decode_cell_data(cell_data, field_type, field_meta)
.unwrap()
.to_string(),
expected_str.to_owned()
);
}
}

View File

@ -95,7 +95,7 @@ impl SelectOptionOperation for SingleSelectTypeOption {
}
}
impl CellDataOperation<String, String> for SingleSelectTypeOption {
impl CellDataOperation<String> for SingleSelectTypeOption {
fn decode_cell_data<T>(
&self,
encoded_data: T,
@ -193,7 +193,7 @@ impl SelectOptionOperation for MultiSelectTypeOption {
}
}
impl CellDataOperation<String, String> for MultiSelectTypeOption {
impl CellDataOperation<String> for MultiSelectTypeOption {
fn decode_cell_data<T>(
&self,
encoded_data: T,

View File

@ -27,11 +27,11 @@ impl TypeOptionBuilder for RichTextTypeOptionBuilder {
#[derive(Debug, Clone, Default, Serialize, Deserialize, ProtoBuf)]
pub struct RichTextTypeOption {
#[pb(index = 1)]
data: String, //It's not used.
data: String, //It's not used yet
}
impl_type_option!(RichTextTypeOption, FieldType::RichText);
impl CellDataOperation<String, String> for RichTextTypeOption {
impl CellDataOperation<String> for RichTextTypeOption {
fn decode_cell_data<T>(
&self,
encoded_data: T,
@ -80,10 +80,10 @@ mod tests {
// date
let field_type = FieldType::DateTime;
let date_time_field_meta = FieldBuilder::from_field_type(&field_type).build();
let json = serde_json::to_string(&DateCellDataSerde::from_timestamp(1647251762, None)).unwrap();
assert_eq!(
type_option
.decode_cell_data(json, &field_type, &date_time_field_meta)
.decode_cell_data(1647251762.to_string(), &field_type, &date_time_field_meta)
.unwrap()
.parse::<DateCellData>()
.unwrap()

View File

@ -30,11 +30,11 @@ impl TypeOptionBuilder for URLTypeOptionBuilder {
#[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)]
pub struct URLTypeOption {
#[pb(index = 1)]
data: String, //It's not used.
data: String, //It's not used yet.
}
impl_type_option!(URLTypeOption, FieldType::URL);
impl CellDataOperation<EncodedCellData<URLCellData>, String> for URLTypeOption {
impl CellDataOperation<EncodedCellData<URLCellData>> for URLTypeOption {
fn decode_cell_data<T>(
&self,
encoded_data: T,
@ -56,31 +56,34 @@ impl CellDataOperation<EncodedCellData<URLCellData>, String> for URLTypeOption {
C: Into<CellContentChangeset>,
{
let changeset = changeset.into();
let mut cell_data = URLCellData {
url: "".to_string(),
content: changeset.to_string(),
};
let mut url = "".to_string();
if let Ok(Some(m)) = URL_REGEX.find(&changeset) {
url = auto_append_scheme(m.as_str());
}
URLCellData {
url,
content: changeset.to_string(),
}
.to_json()
}
}
fn auto_append_scheme(s: &str) -> String {
// Only support https scheme by now
match url::Url::parse(m.as_str()) {
match url::Url::parse(s) {
Ok(url) => {
if url.scheme() == "https" {
cell_data.url = url.into();
url.into()
} else {
cell_data.url = format!("https://{}", m.as_str());
format!("https://{}", s)
}
}
Err(_) => {
cell_data.url = format!("https://{}", m.as_str());
format!("https://{}", s)
}
}
}
cell_data.to_json()
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)]
pub struct URLCellData {
#[pb(index = 1)]

View File

@ -487,6 +487,35 @@ impl GridMetaEditor {
self.grid_pad.read().await.delta_bytes()
}
pub async fn duplicate_grid(&self) -> FlowyResult<BuildGridContext> {
let grid_pad = self.grid_pad.read().await;
let original_blocks = grid_pad.get_block_metas();
let (duplicated_fields, duplicated_blocks) = grid_pad.duplicate_grid_meta().await;
let mut blocks_meta_data = vec![];
if original_blocks.len() == duplicated_blocks.len() {
for (index, original_block_meta) in original_blocks.iter().enumerate() {
let grid_block_meta_editor = self.block_manager.get_editor(&original_block_meta.block_id).await?;
let duplicated_block_id = &duplicated_blocks[index].block_id;
tracing::trace!("Duplicate block:{} meta data", duplicated_block_id);
let duplicated_block_meta_data = grid_block_meta_editor
.duplicate_block_meta_data(duplicated_block_id)
.await;
blocks_meta_data.push(duplicated_block_meta_data);
}
} else {
debug_assert_eq!(original_blocks.len(), duplicated_blocks.len());
}
drop(grid_pad);
Ok(BuildGridContext {
field_metas: duplicated_fields,
blocks: duplicated_blocks,
blocks_meta_data,
})
}
async fn modify<F>(&self, f: F) -> FlowyResult<()>
where
F: for<'a> FnOnce(&'a mut GridMetaPad) -> FlowyResult<Option<GridChangeset>>,

View File

@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use std::fmt::Formatter;
use std::str::FromStr;
pub trait CellDataOperation<D, CO: ToString> {
pub trait CellDataOperation<ED> {
fn decode_cell_data<T>(
&self,
encoded_data: T,
@ -14,14 +14,14 @@ pub trait CellDataOperation<D, CO: ToString> {
field_meta: &FieldMeta,
) -> FlowyResult<DecodedCellData>
where
T: Into<D>;
T: Into<ED>;
//
fn apply_changeset<C: Into<CellContentChangeset>>(
&self,
changeset: C,
cell_meta: Option<CellMeta>,
) -> FlowyResult<CO>;
) -> FlowyResult<String>;
}
#[derive(Debug)]
@ -128,9 +128,7 @@ pub fn apply_cell_data_changeset<T: Into<CellContentChangeset>>(
let s = match field_meta.field_type {
FieldType::RichText => RichTextTypeOption::from(field_meta).apply_changeset(changeset, cell_meta),
FieldType::Number => NumberTypeOption::from(field_meta).apply_changeset(changeset, cell_meta),
FieldType::DateTime => DateTypeOption::from(field_meta)
.apply_changeset(changeset, cell_meta)
.map(|data| data.to_string()),
FieldType::DateTime => DateTypeOption::from(field_meta).apply_changeset(changeset, cell_meta),
FieldType::SingleSelect => SingleSelectTypeOption::from(field_meta).apply_changeset(changeset, cell_meta),
FieldType::MultiSelect => MultiSelectTypeOption::from(field_meta).apply_changeset(changeset, cell_meta),
FieldType::Checkbox => CheckboxTypeOption::from(field_meta).apply_changeset(changeset, cell_meta),

View File

@ -173,7 +173,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
})
}
fn delta_bytes(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
fn view_delta_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
let view_id = view_id.to_string();
let manager = self.0.clone();
FutureResult::new(async move {
@ -197,7 +197,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
})
}
fn process_create_view_data(
fn process_view_delta_data(
&self,
_user_id: &str,
_view_id: &str,
@ -245,13 +245,13 @@ impl ViewDataProcessor for GridViewDataProcessor {
})
}
fn delta_bytes(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
fn view_delta_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
let view_id = view_id.to_string();
let grid_manager = self.0.clone();
FutureResult::new(async move {
let editor = grid_manager.open_grid(view_id).await?;
let delta_bytes = editor.delta_bytes().await;
Ok(delta_bytes)
let delta_bytes = editor.duplicate_grid().await?;
Ok(delta_bytes.into())
})
}
@ -264,7 +264,7 @@ impl ViewDataProcessor for GridViewDataProcessor {
FutureResult::new(async move { make_grid_view_data(&user_id, &view_id, grid_manager, build_context).await })
}
fn process_create_view_data(&self, user_id: &str, view_id: &str, data: Vec<u8>) -> FutureResult<Bytes, FlowyError> {
fn process_view_delta_data(&self, user_id: &str, view_id: &str, data: Vec<u8>) -> FutureResult<Bytes, FlowyError> {
let user_id = user_id.to_string();
let view_id = view_id.to_string();
let grid_manager = self.0.clone();

View File

@ -9,16 +9,16 @@ pub fn create(block_manager: Arc<TextBlockManager>) -> Module {
let mut module = Module::new().name(env!("CARGO_PKG_NAME")).data(block_manager);
module = module
.event(BlockEvent::GetBlockData, get_block_data_handler)
.event(BlockEvent::ApplyDelta, apply_delta_handler)
.event(BlockEvent::ExportDocument, export_handler);
.event(TextBlockEvent::GetBlockData, get_block_data_handler)
.event(TextBlockEvent::ApplyDelta, apply_delta_handler)
.event(TextBlockEvent::ExportDocument, export_handler);
module
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
#[event_err = "FlowyError"]
pub enum BlockEvent {
pub enum TextBlockEvent {
#[event(input = "TextBlockId", output = "TextBlockDelta")]
GetBlockData = 0,

Some files were not shown because too many files have changed in this diff Show More