mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge branch 'upstream-main' into feat/appflowy_tauri_3
# Conflicts: # frontend/appflowy_tauri/src/appflowy_app/components/TestApiButton/TestGrid.tsx # frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts
This commit is contained in:
commit
aab62f4048
@ -1,21 +1,48 @@
|
||||
# appflowy_flutter
|
||||
<h1 align="center" style="margin:0"> AppFlowy_Flutter</h1>
|
||||
<div align="center">
|
||||
<img src="https://img.shields.io/badge/Flutter-v3.3.10-blue"/>
|
||||
<img src="https://img.shields.io/badge/Rust-v1.65-orange"/>
|
||||
</div>
|
||||
|
||||
A new Flutter project.
|
||||
> Documentation for Contributors
|
||||
|
||||
## Getting Started
|
||||
This Repository contains the codebase for the frontend of the application, currently we use Flutter as our frontend framework.
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
### Platforms Supported Using Flutter 💻
|
||||
- Linux
|
||||
- macOS
|
||||
- Windows
|
||||
> We later expect to extend support to Android and iOS devices using Flutter.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
|
||||
|
||||
For help getting started with Flutter, view our
|
||||
[online documentation](https://flutter.dev/docs), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
### Am I Eligible to Contribute?
|
||||
Yes! You are eligible to contribute, check out the ways in which you can [contribute to AppFlowy](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy). Some of the ways in which you can contribute are:
|
||||
- Non-Coding Contributions
|
||||
- Documentation
|
||||
- Feature Requests and Feedbacks
|
||||
- Report Bugs
|
||||
- Improve Translations
|
||||
- Coding Contributions
|
||||
|
||||
|
||||
To contribute to `AppFlowy_Flutter` codebase specifically (coding contribution) we suggest you to have basic knowledge of Flutter. In case you are new to Flutter, we may suggest you to learn the basics and then try to contribute, get started with Flutter [here](https://flutter.dev/docs/get-started/codelab).
|
||||
|
||||
### What OS Should I Use for Development?
|
||||
We support all OS for Development i.e Linux, macOS and Windows. However, most of us promote macOS and Linux over Windows. We have detailed [docs](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/environment-setup) on How to Setup `AppFlowy_Flutter` in your local system in each OS.
|
||||
|
||||
|
||||
### Getting Started ❇
|
||||
We have a detailed documentation, on how to [get started](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) with the project, and make your first contribution. However, we do have some specific picks for you.
|
||||
- [Code Architecture](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/frontend/codemap)
|
||||
- [Making Your First PR](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/submitting-your-first-pull-request)
|
||||
- [The Style Guide](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/style-guides)
|
||||
- [How to run/debug the application](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/launcher-and-tasks)
|
||||
|
||||
|
||||
### Need Help?
|
||||
- New to GitHub? Follow [these](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/setting-up-your-repositories) steps to get started
|
||||
- Stuck Somewhere? Join the [Discord](https://discord.gg/9Q2xaN37tV) Group and we are there to help you!
|
||||
|
||||
<!--
|
||||
## release check
|
||||
1. [entitlements](https://flutter.dev/desktop#setting-up-entitlements)
|
||||
2. [symbols stripped](https://flutter.dev/docs/development/platform-integration/c-interop)
|
||||
2. [symbols stripped](https://flutter.dev/docs/development/platform-integration/c-interop) -->
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"appName": "AppFlowy",
|
||||
"defaultUsername": "Eu",
|
||||
"welcomeText": "Bem-vindo @:appName",
|
||||
"welcomeText": "Bem-vindo ao @:appName",
|
||||
"githubStarText": "Dar uma estrela no Github",
|
||||
"subscribeNewsletterText": "Inscreva-se para receber novidades",
|
||||
"letsGoButtonText": "Vamos lá",
|
||||
@ -30,15 +30,22 @@
|
||||
"unmatchedPasswordError": "As senhas não conferem."
|
||||
},
|
||||
"workspace": {
|
||||
"create": "Crie uma área de trabalho",
|
||||
"create": "Crie um espaço de trabalho",
|
||||
"hint": "Espaço de trabalho",
|
||||
"notFoundError": "Espaço de trabalho não encontrada"
|
||||
"notFoundError": "Espaço de trabalho não encontrado"
|
||||
},
|
||||
"shareAction": {
|
||||
"buttonText": "Compartilhar",
|
||||
"workInProgress": "Em breve",
|
||||
"markdown": "Marcador",
|
||||
"copyLink": "Copiar o link"
|
||||
"copyLink": "Copiar link"
|
||||
},
|
||||
"moreAction": {
|
||||
"small": "pequeno",
|
||||
"medium": "médio",
|
||||
"large": "grande",
|
||||
"fontSize": "Tamanho da fonte",
|
||||
"import": "Importar"
|
||||
},
|
||||
"disclosureAction": {
|
||||
"rename": "Renomear",
|
||||
@ -64,6 +71,7 @@
|
||||
},
|
||||
"dialogCreatePageNameHint": "Nome da página",
|
||||
"questionBubble": {
|
||||
"shortcuts": "Atalhos",
|
||||
"whatsNew": "O que há de novo?",
|
||||
"help": "Ajuda e Suporte",
|
||||
"debug": {
|
||||
@ -90,14 +98,17 @@
|
||||
"inlineCode": "Embutir código",
|
||||
"quote": "Citação em bloco",
|
||||
"header": "Cabeçalho",
|
||||
"highlight": "Realçar"
|
||||
"highlight": "Destacar",
|
||||
"color": "Cor"
|
||||
},
|
||||
"tooltip": {
|
||||
"lightMode": "Mudar para o modo claro",
|
||||
"darkMode": "Mudar para o modo escuro",
|
||||
"openAsPage": "Abrir como uma página",
|
||||
"addNewRow": "Adicionar uma nova linha",
|
||||
"openMenu": "Clique para abrir o menu"
|
||||
"openMenu": "Clique para abrir o menu",
|
||||
"viewDataBase": "Visualizar banco de dados",
|
||||
"referencePage": "Esta {name} é uma referência"
|
||||
},
|
||||
"sideBar": {
|
||||
"openSidebar": "Abrir barra lateral",
|
||||
@ -121,10 +132,16 @@
|
||||
"signIn": "Conectar",
|
||||
"signOut": "Desconectar",
|
||||
"complete": "Completar",
|
||||
"save": "Salvar"
|
||||
"save": "Salvar",
|
||||
"generate": "Gerar",
|
||||
"esc": "Sair",
|
||||
"keep": "Manter",
|
||||
"tryAGain": "Tentar novamente",
|
||||
"discard": "Descartar",
|
||||
"replace": "substituir"
|
||||
},
|
||||
"label": {
|
||||
"welcome": "Welcome!",
|
||||
"welcome": "Bem-vindo!",
|
||||
"firstName": "Nome",
|
||||
"middleName": "Sobrenome",
|
||||
"lastName": "Último nome",
|
||||
@ -149,28 +166,102 @@
|
||||
"appearance": "Aparência",
|
||||
"language": "Idioma",
|
||||
"user": "Usuário",
|
||||
"open": "Abrir as Configurações"
|
||||
"files": "Arquivos",
|
||||
"open": "Abrir Configurações"
|
||||
},
|
||||
"appearance": {
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "Modo Claro",
|
||||
"dark": "Modo Escuro",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
"label": "Tema",
|
||||
"light": "Modo claro",
|
||||
"dark": "Modo escuro",
|
||||
"system": "Adaptar-se ao sistema"
|
||||
},
|
||||
"theme": "Tema"
|
||||
},
|
||||
"files": {
|
||||
"defaultLocation": "Onde os seus dados ficam armazenados",
|
||||
"doubleTapToCopy": "Clique duas vezes para copiar o caminho",
|
||||
"restoreLocation": "Restaurar para o caminho padrão do AppFlowy",
|
||||
"customizeLocation": "Abrir outra pasta",
|
||||
"restartApp": "Reinicie o aplicativo para que as alterações entrem em vigor.",
|
||||
"exportDatabase": "Exportar banco de dados",
|
||||
"selectFiles": "Escolha os arquivos que precisam ser exportados",
|
||||
"createNewFolder": "Criar uma nova pasta",
|
||||
"createNewFolderDesc": "Diga-nos onde pretende armazenar os seus dados ...",
|
||||
"open": "Abrir",
|
||||
"openFolder": "Abra uma pasta existente",
|
||||
"openFolderDesc": "Gravar na pasta AppFlowy existente ...",
|
||||
"folderHintText": "nome da pasta",
|
||||
"location": "Criando nova pasta",
|
||||
"locationDesc": "Escolha um nome para sua pasta de dados do AppFlowy",
|
||||
"browser": "Navegar",
|
||||
"create": "Criar",
|
||||
"folderPath": "Caminho para armazenar sua pasta",
|
||||
"locationCannotBeEmpty": "O caminho não pode estar vazio"
|
||||
},
|
||||
"user": {
|
||||
"name": "Nome",
|
||||
"icon": "Ícone",
|
||||
"selectAnIcon": "Escolha um ícone",
|
||||
"pleaseInputYourOpenAIKey": "por favor insira sua chave OpenAI"
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
"settings": {
|
||||
"filter": "Filtro",
|
||||
"sortBy": "Ordenar por",
|
||||
"sort": "Organizar",
|
||||
"sortBy": "Organizar por",
|
||||
"Properties": "Propriedades",
|
||||
"group": "Grupo"
|
||||
"group": "Grupo",
|
||||
"addFilter": "Adicionar filtro",
|
||||
"deleteFilter": "Apagar filtro",
|
||||
"filterBy": "Filtrar por...",
|
||||
"typeAValue": "Digite um valor..."
|
||||
},
|
||||
"textFilter": {
|
||||
"contains": "Contém",
|
||||
"doesNotContain": "Não contém",
|
||||
"endsWith": "Termina com",
|
||||
"startWith": "Inicia com",
|
||||
"is": "É",
|
||||
"isNot": "Não é",
|
||||
"isEmpty": "Está vazio",
|
||||
"isNotEmpty": "Não está vazio",
|
||||
"choicechipPrefix": {
|
||||
"isNot": "Não",
|
||||
"startWith": "Inicia com",
|
||||
"endWith": "Termina com",
|
||||
"isEmpty": "está vazio",
|
||||
"isNotEmpty": "não está vazio"
|
||||
}
|
||||
},
|
||||
"checkboxFilter": {
|
||||
"isChecked": "Marcado",
|
||||
"isUnchecked": "Desmarcado",
|
||||
"choicechipPrefix": {
|
||||
"is": "está"
|
||||
}
|
||||
},
|
||||
"checklistFilter": {
|
||||
"isComplete": "está completo",
|
||||
"isIncomplted": "está imcompleto"
|
||||
},
|
||||
"singleSelectOptionFilter": {
|
||||
"is": "Está",
|
||||
"isNot": "Não está",
|
||||
"isEmpty": "Está vazio",
|
||||
"isNotEmpty": "Não está vazio"
|
||||
},
|
||||
"multiSelectOptionFilter": {
|
||||
"contains": "Contém",
|
||||
"doesNotContain": "Não contém",
|
||||
"isEmpty": "Está vazio",
|
||||
"isNotEmpty": "Está vazio"
|
||||
},
|
||||
"field": {
|
||||
"hide": "Esconder",
|
||||
"insertLeft": "Inserir à esquerda",
|
||||
"insertRight": "Inserir à direita",
|
||||
"hide": "Ocultar",
|
||||
"insertLeft": "Inserir a esquerda",
|
||||
"insertRight": "Inserir a direita",
|
||||
"duplicate": "Duplicar",
|
||||
"delete": "Apagar",
|
||||
"textFieldName": "Texto",
|
||||
@ -178,32 +269,40 @@
|
||||
"dateFieldName": "Data",
|
||||
"numberFieldName": "Números",
|
||||
"singleSelectFieldName": "Selecionar",
|
||||
"multiSelectFieldName": "Seleção múltipla",
|
||||
"multiSelectFieldName": "Multi seleção",
|
||||
"urlFieldName": "URL",
|
||||
"checklistFieldName": "Lista",
|
||||
"numberFormat": "Formato numérico",
|
||||
"dateFormat": "Formato de data",
|
||||
"includeTime": "Incluir horário",
|
||||
"dateFormatFriendly": "Mês/Dia/Ano",
|
||||
"dateFormatISO": "Ano/Mês/Dia",
|
||||
"includeTime": "Incluir hora",
|
||||
"dateFormatFriendly": "Mês Dia,Ano",
|
||||
"dateFormatISO": "Ano-Mês-Dia",
|
||||
"dateFormatLocal": "Mês/Dia/Ano",
|
||||
"dateFormatUS": "Ano/Mês/Dia",
|
||||
"timeFormat": "Formato de hora",
|
||||
"invalidTimeFormat": "Formato Inválido",
|
||||
"invalidTimeFormat": "Formato inválido",
|
||||
"timeFormatTwelveHour": "12 horas",
|
||||
"timeFormatTwentyFourHour": "24 horas",
|
||||
"addSelectOption": "Adicionar uma opção",
|
||||
"optionTitle": "Opções",
|
||||
"addOption": "Adicionar opção",
|
||||
"addOption": "Adicioar opção",
|
||||
"editProperty": "Editar propriedade",
|
||||
"newColumn": "Nova coluna",
|
||||
"deleteFieldPromptMessage": "Tem certeza? Esta propriedade será excluída"
|
||||
},
|
||||
"sort": {
|
||||
"ascending": "Crescente",
|
||||
"descending": "Decrescente",
|
||||
"deleteSort": "Apagar ordenação",
|
||||
"addSort": "Adicionar ordenação"
|
||||
},
|
||||
"row": {
|
||||
"duplicate": "Duplicar",
|
||||
"delete": "Apagar",
|
||||
"textPlaceholder": "Vazio",
|
||||
"copyProperty": "Propriedade copiada para a área de transferência",
|
||||
"count": "Contagem"
|
||||
"count": "Contagem",
|
||||
"newRow": "Nova linha"
|
||||
},
|
||||
"selectOption": {
|
||||
"create": "Criar",
|
||||
@ -219,7 +318,10 @@
|
||||
"deleteTag": "Apagar etiqueta",
|
||||
"colorPannelTitle": "Cores",
|
||||
"pannelTitle": "Escolha uma opção ou crie uma",
|
||||
"searchOption": "Procure uma opção"
|
||||
"searchOption": "Procurar uma opção"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "Adicionar um item"
|
||||
},
|
||||
"menuName": "Grade"
|
||||
},
|
||||
@ -228,11 +330,45 @@
|
||||
"date": {
|
||||
"timeHintTextInTwelveHour": "01:00 PM",
|
||||
"timeHintTextInTwentyFourHour": "13:00"
|
||||
},
|
||||
"slashMenu": {
|
||||
"board": {
|
||||
"selectABoardToLinkTo": "Selecione um quadro para vincular"
|
||||
},
|
||||
"grid": {
|
||||
"selectAGridToLinkTo": "Selecione um grade para vincular"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"referencedBoard": "Quadro vinculado",
|
||||
"referencedGrid": "Grade vinculado",
|
||||
"autoCompletionMenuItemName": "Preenchimento Automático",
|
||||
"autoGeneratorMenuItemName": "Gerar nome automaticamente",
|
||||
"autoGeneratorTitleName": "Gerar por IA",
|
||||
"autoGeneratorLearnMore": "Saiba mais",
|
||||
"autoGeneratorGenerate": "Gerar",
|
||||
"autoGeneratorHintText": "Diga-nos o que você deseja gerar por IA ...",
|
||||
"autoGeneratorCantGetOpenAIKey": "Não foi possível obter a chave da OpenAI",
|
||||
"smartEditTitleName": "IA: edição inteligente",
|
||||
"smartEditFixSpelling": "Corrigir ortografia",
|
||||
"smartEditSummarize": "Resumir",
|
||||
"smartEditCouldNotFetchResult": "Não foi possível obter o resultado do OpenAI",
|
||||
"smartEditCouldNotFetchKey": "Não foi possível obter a chave OpenAI"
|
||||
}
|
||||
},
|
||||
"board": {
|
||||
"column": {
|
||||
"create_new_card": "Novo"
|
||||
},
|
||||
"menuName": "Quadro"
|
||||
},
|
||||
"calendar": {
|
||||
"menuName": "Calendário",
|
||||
"navigation": {
|
||||
"today": "Hoje",
|
||||
"jumpToday": "Pular para hoje",
|
||||
"previousMonth": "Mês anterior",
|
||||
"nextMonth": "Próximo mês"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/group.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
|
||||
|
||||
@ -60,6 +60,6 @@ class DatabaseBackendService {
|
||||
|
||||
Future<Either<RepeatedGroupPB, FlowyError>> loadGroups() {
|
||||
final payload = DatabaseViewIdPB(value: viewId);
|
||||
return DatabaseEventGetGroup(payload).send();
|
||||
return DatabaseEventGetGroups(payload).send();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
|
||||
import '../grid/presentation/widgets/filter/filter_info.dart';
|
||||
|
@ -1,8 +1,8 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'field_service.freezed.dart';
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
@ -6,7 +7,6 @@ import 'package:appflowy_backend/protobuf/flowy-database/checkbox_filter.pbserve
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/checklist_filter.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/date_filter.pbserver.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/number_filter.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/select_option_filter.pbserver.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/group_changeset.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/group.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart';
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/grid_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/sort_entities.pb.dart';
|
||||
|
||||
|
@ -68,9 +68,8 @@ class GroupController {
|
||||
|
||||
if (index != -1) {
|
||||
group.rows[index] = updatedRow;
|
||||
delegate.updateRow(group, updatedRow);
|
||||
}
|
||||
|
||||
delegate.updateRow(group, updatedRow);
|
||||
}
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
@ -78,29 +77,6 @@ class GroupController {
|
||||
});
|
||||
}
|
||||
|
||||
// GroupChangesetPB _transformChangeset(GroupChangesetPB changeset) {
|
||||
// final insertedRows = changeset.insertedRows
|
||||
// .where(
|
||||
// (delete) => !changeset.deletedRows.contains(delete.row.id),
|
||||
// )
|
||||
// .toList();
|
||||
|
||||
// final deletedRows = changeset.deletedRows
|
||||
// .where((deletedRowId) =>
|
||||
// changeset.insertedRows
|
||||
// .indexWhere((insert) => insert.row.id == deletedRowId) ==
|
||||
// -1)
|
||||
// .toList();
|
||||
|
||||
// return changeset.rebuild((rebuildChangeset) {
|
||||
// rebuildChangeset.insertedRows.clear();
|
||||
// rebuildChangeset.insertedRows.addAll(insertedRows);
|
||||
|
||||
// rebuildChangeset.deletedRows.clear();
|
||||
// rebuildChangeset.deletedRows.addAll(deletedRows);
|
||||
// });
|
||||
// }
|
||||
|
||||
Future<void> dispose() async {
|
||||
_listener.stop();
|
||||
}
|
||||
|
@ -1,15 +1,18 @@
|
||||
import 'dart:convert';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
|
||||
import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:appflowy/plugins/document/application/doc_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
show EditorState, Document, Transaction;
|
||||
show EditorState, Document, Transaction, Node;
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
@ -78,29 +81,27 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
Future<void> _initial(Initial value, Emitter<DocumentState> emit) async {
|
||||
final userProfile = await UserBackendService.getCurrentUserProfile();
|
||||
if (userProfile.isRight()) {
|
||||
emit(
|
||||
return emit(
|
||||
state.copyWith(
|
||||
loadingState: DocumentLoadingState.finish(
|
||||
right(userProfile.asRight()),
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final result = await _documentService.openDocument(view: view);
|
||||
result.fold(
|
||||
(documentData) {
|
||||
final document = Document.fromJson(jsonDecode(documentData.content));
|
||||
editorState = EditorState(document: document);
|
||||
_listenOnDocumentChange();
|
||||
emit(
|
||||
state.copyWith(
|
||||
loadingState: DocumentLoadingState.finish(left(unit)),
|
||||
userProfilePB: userProfile.asLeft(),
|
||||
),
|
||||
);
|
||||
return result.fold(
|
||||
(documentData) async {
|
||||
await _initEditorState(documentData).whenComplete(() {
|
||||
emit(
|
||||
state.copyWith(
|
||||
loadingState: DocumentLoadingState.finish(left(unit)),
|
||||
userProfilePB: userProfile.asLeft(),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
(err) {
|
||||
(err) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
loadingState: DocumentLoadingState.finish(right(err)),
|
||||
@ -127,8 +128,13 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
);
|
||||
}
|
||||
|
||||
void _listenOnDocumentChange() {
|
||||
_subscription = editorState?.transactionStream.listen((transaction) {
|
||||
Future<void> _initEditorState(DocumentDataPB documentData) async {
|
||||
final document = Document.fromJson(jsonDecode(documentData.content));
|
||||
final editorState = EditorState(document: document);
|
||||
this.editorState = editorState;
|
||||
|
||||
// listen on document change
|
||||
_subscription = editorState.transactionStream.listen((transaction) {
|
||||
final json = jsonEncode(TransactionAdaptor(transaction).toJson());
|
||||
_documentService
|
||||
.applyEdit(docId: view.id, operations: json)
|
||||
@ -139,6 +145,15 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
);
|
||||
});
|
||||
});
|
||||
// log
|
||||
if (kDebugMode) {
|
||||
editorState.logConfiguration.handler = (log) {
|
||||
Log.debug(log);
|
||||
};
|
||||
}
|
||||
// migration
|
||||
final migration = DocumentMigration(editorState: editorState);
|
||||
await migration.apply();
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,3 +230,33 @@ class TransactionAdaptor {
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentMigration {
|
||||
const DocumentMigration({
|
||||
required this.editorState,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
|
||||
/// Migrate the document to the latest version.
|
||||
Future<void> apply() async {
|
||||
final transaction = editorState.transaction;
|
||||
|
||||
// A temporary solution to migrate the document to the latest version.
|
||||
// Once the editor is stable, we can remove this.
|
||||
|
||||
// cover plugin
|
||||
if (editorState.document.nodeAtPath([0])?.type != kCoverType) {
|
||||
transaction.insertNode(
|
||||
[0],
|
||||
Node(type: kCoverType),
|
||||
);
|
||||
}
|
||||
|
||||
transaction.afterSelection = null;
|
||||
|
||||
if (transaction.operations.isNotEmpty) {
|
||||
editorState.apply(transaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,7 +91,13 @@ class HttpOpenAIRepository implements OpenAIRepository {
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return Right(TextCompletionResponse.fromJson(json.decode(response.body)));
|
||||
return Right(
|
||||
TextCompletionResponse.fromJson(
|
||||
json.decode(
|
||||
utf8.decode(response.bodyBytes),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Left(OpenAIError.fromJson(json.decode(response.body)['error']));
|
||||
}
|
||||
@ -119,7 +125,13 @@ class HttpOpenAIRepository implements OpenAIRepository {
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return Right(TextEditResponse.fromJson(json.decode(response.body)));
|
||||
return Right(
|
||||
TextEditResponse.fromJson(
|
||||
json.decode(
|
||||
utf8.decode(response.bodyBytes),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Left(OpenAIError.fromJson(json.decode(response.body)['error']));
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
import 'package:appflowy/plugins/document/document.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/app/header/import/import_panel.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' show Document, Node;
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' show Document;
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
@ -61,12 +60,7 @@ class AddButton extends StatelessWidget {
|
||||
},
|
||||
onSelected: (action, controller) {
|
||||
if (action is AddButtonActionWrapper) {
|
||||
Document? document;
|
||||
if (action.pluginType == PluginType.editor) {
|
||||
// initialize the document if needed.
|
||||
document = buildInitialDocument();
|
||||
}
|
||||
onSelected(action.pluginBuilder, document);
|
||||
onSelected(action.pluginBuilder, null);
|
||||
}
|
||||
if (action is ImportActionWrapper) {
|
||||
showImportPanel(context, (document) {
|
||||
@ -80,12 +74,6 @@ class AddButton extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Document buildInitialDocument() {
|
||||
final document = Document.empty();
|
||||
document.insert([0], [Node(type: kCoverType)]);
|
||||
return document;
|
||||
}
|
||||
}
|
||||
|
||||
class AddButtonActionWrapper extends ActionCell {
|
||||
|
@ -12,8 +12,9 @@ ShortcutEventHandler backspaceEventHandler = (editorState, event) {
|
||||
nodes = selection.isBackward ? nodes : nodes.reversed.toList(growable: false);
|
||||
selection = selection.isBackward ? selection : selection.reversed;
|
||||
final textNodes = nodes.whereType<TextNode>().toList();
|
||||
final List<Node> nonTextNodes =
|
||||
nodes.where((node) => node is! TextNode).toList(growable: false);
|
||||
final List<Node> nonTextNodes = nodes
|
||||
.where((node) => node is! TextNode && node.selectable != null)
|
||||
.toList(growable: false);
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
List<int>? cancelNumberListPath;
|
||||
|
@ -54,6 +54,37 @@ void main() async {
|
||||
'b': 3,
|
||||
'c': 4,
|
||||
});
|
||||
expect(invertAttributes(null, base), {
|
||||
'a': null,
|
||||
'b': null,
|
||||
});
|
||||
expect(invertAttributes(other, null), {
|
||||
'b': 3,
|
||||
'c': 4,
|
||||
});
|
||||
});
|
||||
test(
|
||||
"hasAttributes",
|
||||
() {
|
||||
final base = {
|
||||
'a': 1,
|
||||
'b': 2,
|
||||
};
|
||||
final other = {
|
||||
'c': 3,
|
||||
'd': 4,
|
||||
};
|
||||
|
||||
var x = hashAttributes(base);
|
||||
var y = hashAttributes(base);
|
||||
// x & y should have same hash code
|
||||
expect(x == y, true);
|
||||
|
||||
y = hashAttributes(other);
|
||||
|
||||
// x & y should have different hash code
|
||||
expect(x == y, false);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -29,5 +29,43 @@ void main() async {
|
||||
expect(p2 <= p1, true);
|
||||
expect(p1.equals(p2), true);
|
||||
});
|
||||
test(
|
||||
"test path next, previous and parent getters",
|
||||
() {
|
||||
var p1 = [0, 0];
|
||||
var p2 = [0, 1];
|
||||
|
||||
expect(p1.next.equals(p2), true);
|
||||
expect(p1.previous.equals(p2), false);
|
||||
expect(p1.parent.equals(p2), false);
|
||||
|
||||
p1 = [0, 1, 0];
|
||||
p2 = [0, 1, 1];
|
||||
|
||||
expect(p2.next.equals(p1), false);
|
||||
expect(p2.previous.equals(p1), true);
|
||||
expect(p2.parent.equals(p1), false);
|
||||
|
||||
p1 = [0, 1, 1];
|
||||
p2 = [0, 1, 1];
|
||||
|
||||
expect(p1.next.equals(p2), false);
|
||||
expect(p1.previous.equals(p2), false);
|
||||
expect(p1.parent.equals(p2), false);
|
||||
|
||||
p1 = [];
|
||||
p2 = [];
|
||||
|
||||
expect(p1.next.equals(p2), true);
|
||||
expect(p2.previous.equals(p1), true);
|
||||
expect(p1.parent.equals(p2), true);
|
||||
|
||||
p1 = [1, 0, 2];
|
||||
p2 = [1, 0];
|
||||
|
||||
expect(p1.parent.equals(p2), true);
|
||||
expect(p2.parent.equals(p1), false);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import { SignUpPage } from './views/SignUpPage';
|
||||
import { ConfirmAccountPage } from './views/ConfirmAccountPage';
|
||||
import { ErrorHandlerPage } from './components/error/ErrorHandlerPage';
|
||||
import initializeI18n from './stores/i18n/initializeI18n';
|
||||
import { TestAPI } from './components/TestApiButton/TestAPI';
|
||||
import { TestAPI } from './components/tests/TestAPI';
|
||||
import { GetStarted } from './components/auth/GetStarted/GetStarted';
|
||||
|
||||
initializeI18n();
|
||||
|
@ -1,334 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
FieldType,
|
||||
NumberFormat,
|
||||
NumberTypeOptionPB,
|
||||
SelectOptionCellDataPB,
|
||||
SingleSelectTypeOptionPB,
|
||||
ViewLayoutTypePB,
|
||||
} from '../../../services/backend';
|
||||
import { Log } from '../../utils/log';
|
||||
import {
|
||||
assertFieldName,
|
||||
assertNumberOfFields,
|
||||
assertNumberOfRows,
|
||||
assertTextCell,
|
||||
createTestDatabaseView,
|
||||
editTextCell,
|
||||
findFirstFieldInfoWithFieldType,
|
||||
makeMultiSelectCellController,
|
||||
makeSingleSelectCellController,
|
||||
makeTextCellController,
|
||||
openTestDatabase,
|
||||
} from './DatabaseTestHelper';
|
||||
import {
|
||||
SelectOptionBackendService,
|
||||
SelectOptionCellBackendService,
|
||||
} from '../../stores/effects/database/cell/select_option_bd_svc';
|
||||
import { TypeOptionController } from '../../stores/effects/database/field/type_option/type_option_controller';
|
||||
import { None, Some } from 'ts-results';
|
||||
import { RowBackendService } from '../../stores/effects/database/row/row_bd_svc';
|
||||
import {
|
||||
makeNumberTypeOptionContext,
|
||||
makeSingleSelectTypeOptionContext,
|
||||
} from '../../stores/effects/database/field/type_option/type_option_context';
|
||||
|
||||
export const TestCreateGrid = () => {
|
||||
async function createBuildInGrid() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
databaseController.subscribe({
|
||||
onViewChanged: (databasePB) => {
|
||||
Log.debug('Did receive database:' + databasePB);
|
||||
},
|
||||
// onRowsChanged: async (rows) => {
|
||||
// if (rows.length !== 3) {
|
||||
// throw Error('Expected number of rows is 3, but receive ' + rows.length);
|
||||
// }
|
||||
// },
|
||||
onFieldsChanged: (fields) => {
|
||||
if (fields.length !== 3) {
|
||||
throw Error('Expected number of fields is 3, but receive ' + fields.length);
|
||||
}
|
||||
},
|
||||
});
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
return TestButton('Test create build-in grid', createBuildInGrid);
|
||||
};
|
||||
|
||||
export const TestEditCell = () => {
|
||||
async function testGridRow() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
|
||||
for (const [index, row] of databaseController.databaseViewCache.rowInfos.entries()) {
|
||||
const cellContent = index.toString();
|
||||
const fieldInfo = findFirstFieldInfoWithFieldType(row, FieldType.RichText).unwrap();
|
||||
await editTextCell(fieldInfo.field.id, row, databaseController, cellContent);
|
||||
await assertTextCell(fieldInfo.field.id, row, databaseController, cellContent);
|
||||
}
|
||||
}
|
||||
|
||||
return TestButton('Test editing cell', testGridRow);
|
||||
};
|
||||
|
||||
export const TestCreateRow = () => {
|
||||
async function testCreateRow() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
await assertNumberOfRows(view.id, 3);
|
||||
|
||||
// Create a row from a DatabaseController or create using the RowBackendService
|
||||
await databaseController.createRow();
|
||||
await assertNumberOfRows(view.id, 4);
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
return TestButton('Test create row', testCreateRow);
|
||||
};
|
||||
export const TestDeleteRow = () => {
|
||||
async function testDeleteRow() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
|
||||
const rows = databaseController.databaseViewCache.rowInfos;
|
||||
const svc = new RowBackendService(view.id);
|
||||
await svc.deleteRow(rows[0].row.id);
|
||||
await assertNumberOfRows(view.id, 2);
|
||||
|
||||
// Wait the databaseViewCache get the change notification and
|
||||
// update the rows.
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
if (databaseController.databaseViewCache.rowInfos.length !== 2) {
|
||||
throw Error('The number of rows is not match');
|
||||
}
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
return TestButton('Test delete row', testDeleteRow);
|
||||
};
|
||||
export const TestCreateSelectOptionInCell = () => {
|
||||
async function testCreateOptionInCell() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
for (const [index, row] of databaseController.databaseViewCache.rowInfos.entries()) {
|
||||
if (index === 0) {
|
||||
const fieldInfo = findFirstFieldInfoWithFieldType(row, FieldType.SingleSelect).unwrap();
|
||||
const cellController = await makeSingleSelectCellController(fieldInfo.field.id, row, databaseController).then(
|
||||
(result) => result.unwrap()
|
||||
);
|
||||
cellController.subscribeChanged({
|
||||
onCellChanged: (value) => {
|
||||
if (value.some) {
|
||||
const option: SelectOptionCellDataPB = value.unwrap();
|
||||
console.log(option);
|
||||
}
|
||||
},
|
||||
});
|
||||
const backendSvc = new SelectOptionCellBackendService(cellController.cellIdentifier);
|
||||
await backendSvc.createOption({ name: 'option' + index });
|
||||
await cellController.dispose();
|
||||
}
|
||||
}
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
return TestButton('Test create a select option in cell', testCreateOptionInCell);
|
||||
};
|
||||
|
||||
export const TestGetSingleSelectFieldData = () => {
|
||||
async function testGetSingleSelectFieldData() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
|
||||
// Find the single select column
|
||||
const singleSelect = databaseController.fieldController.fieldInfos.find(
|
||||
(fieldInfo) => fieldInfo.field.field_type === FieldType.SingleSelect
|
||||
)!;
|
||||
const typeOptionController = new TypeOptionController(view.id, Some(singleSelect));
|
||||
const singleSelectTypeOptionContext = makeSingleSelectTypeOptionContext(typeOptionController);
|
||||
|
||||
// Create options
|
||||
const singleSelectTypeOptionPB: SingleSelectTypeOptionPB = await singleSelectTypeOptionContext
|
||||
.getTypeOption()
|
||||
.then((result) => result.unwrap());
|
||||
const backendSvc = new SelectOptionBackendService(view.id, singleSelect.field.id);
|
||||
const option1 = await backendSvc.createOption({ name: 'Task 1' }).then((result) => result.unwrap());
|
||||
singleSelectTypeOptionPB.options.splice(0, 0, option1);
|
||||
const option2 = await backendSvc.createOption({ name: 'Task 2' }).then((result) => result.unwrap());
|
||||
singleSelectTypeOptionPB.options.splice(0, 0, option2);
|
||||
const option3 = await backendSvc.createOption({ name: 'Task 3' }).then((result) => result.unwrap());
|
||||
singleSelectTypeOptionPB.options.splice(0, 0, option3);
|
||||
await singleSelectTypeOptionContext.setTypeOption(singleSelectTypeOptionPB);
|
||||
|
||||
// Read options
|
||||
const options = singleSelectTypeOptionPB.options;
|
||||
console.log(options);
|
||||
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
return TestButton('Test get single-select column data', testGetSingleSelectFieldData);
|
||||
};
|
||||
|
||||
export const TestSwitchFromSingleSelectToNumber = () => {
|
||||
async function testSwitchFromSingleSelectToNumber() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
|
||||
// Find the single select column
|
||||
const singleSelect = databaseController.fieldController.fieldInfos.find(
|
||||
(fieldInfo) => fieldInfo.field.field_type === FieldType.SingleSelect
|
||||
)!;
|
||||
const typeOptionController = new TypeOptionController(view.id, Some(singleSelect));
|
||||
await typeOptionController.switchToField(FieldType.Number);
|
||||
|
||||
// Check the number type option
|
||||
const numberTypeOptionContext = makeNumberTypeOptionContext(typeOptionController);
|
||||
const numberTypeOption: NumberTypeOptionPB = await numberTypeOptionContext
|
||||
.getTypeOption()
|
||||
.then((result) => result.unwrap());
|
||||
const format: NumberFormat = numberTypeOption.format;
|
||||
if (format !== NumberFormat.Num) {
|
||||
throw Error('The default format should be number');
|
||||
}
|
||||
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
return TestButton('Test switch from single-select to number column', testSwitchFromSingleSelectToNumber);
|
||||
};
|
||||
|
||||
export const TestSwitchFromMultiSelectToText = () => {
|
||||
async function testSwitchFromMultiSelectToRichText() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
|
||||
// Create multi-select field
|
||||
const typeOptionController = new TypeOptionController(view.id, None, FieldType.MultiSelect);
|
||||
await typeOptionController.initialize();
|
||||
|
||||
// Insert options to first row
|
||||
const row = databaseController.databaseViewCache.rowInfos[0];
|
||||
const multiSelectField = typeOptionController.getFieldInfo();
|
||||
// const multiSelectField = findFirstFieldInfoWithFieldType(row, FieldType.MultiSelect).unwrap();
|
||||
const selectOptionCellController = await makeMultiSelectCellController(
|
||||
multiSelectField.field.id,
|
||||
row,
|
||||
databaseController
|
||||
).then((result) => result.unwrap());
|
||||
const backendSvc = new SelectOptionCellBackendService(selectOptionCellController.cellIdentifier);
|
||||
await backendSvc.createOption({ name: 'A' });
|
||||
await backendSvc.createOption({ name: 'B' });
|
||||
await backendSvc.createOption({ name: 'C' });
|
||||
|
||||
const selectOptionCellData = await selectOptionCellController.getCellData().then((result) => result.unwrap());
|
||||
if (selectOptionCellData.options.length !== 3) {
|
||||
throw Error('The options should equal to 3');
|
||||
}
|
||||
|
||||
if (selectOptionCellData.select_options.length !== 3) {
|
||||
throw Error('The selected options should equal to 3');
|
||||
}
|
||||
await selectOptionCellController.dispose();
|
||||
|
||||
// Switch to RichText field type
|
||||
await typeOptionController.switchToField(FieldType.RichText).then((result) => result.unwrap());
|
||||
if (typeOptionController.fieldType !== FieldType.RichText) {
|
||||
throw Error('The field type should be text');
|
||||
}
|
||||
|
||||
const textCellController = await makeTextCellController(multiSelectField.field.id, row, databaseController).then(
|
||||
(result) => result.unwrap()
|
||||
);
|
||||
const cellContent = await textCellController.getCellData();
|
||||
if (cellContent.unwrap() !== 'A,B,C') {
|
||||
throw Error('The cell content should be A,B,C, but receive: ' + cellContent.unwrap());
|
||||
}
|
||||
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
return TestButton('Test switch from multi-select to text column', testSwitchFromMultiSelectToRichText);
|
||||
};
|
||||
|
||||
export const TestEditField = () => {
|
||||
async function testEditField() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
const fieldInfos = databaseController.fieldController.fieldInfos;
|
||||
|
||||
// Modify the name of the field
|
||||
const firstFieldInfo = fieldInfos[0];
|
||||
const controller = new TypeOptionController(view.id, Some(firstFieldInfo));
|
||||
await controller.initialize();
|
||||
const newName = 'hello world';
|
||||
await controller.setFieldName(newName);
|
||||
|
||||
await assertFieldName(view.id, firstFieldInfo.field.id, firstFieldInfo.field.field_type, newName);
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
return TestButton('Test edit the column name', testEditField);
|
||||
};
|
||||
|
||||
export const TestCreateNewField = () => {
|
||||
async function testCreateNewField() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
await assertNumberOfFields(view.id, 3);
|
||||
|
||||
// Modify the name of the field
|
||||
const controller = new TypeOptionController(view.id, None);
|
||||
await controller.initialize();
|
||||
await assertNumberOfFields(view.id, 4);
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
return TestButton('Test create a new column', testCreateNewField);
|
||||
};
|
||||
|
||||
export const TestDeleteField = () => {
|
||||
async function testDeleteField() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
|
||||
// Modify the name of the field.
|
||||
// The fieldInfos[0] is the primary field by default, we can't delete it.
|
||||
// So let choose the second fieldInfo.
|
||||
const fieldInfo = databaseController.fieldController.fieldInfos[1];
|
||||
const controller = new TypeOptionController(view.id, Some(fieldInfo));
|
||||
await controller.initialize();
|
||||
await assertNumberOfFields(view.id, 3);
|
||||
await controller.deleteField();
|
||||
await assertNumberOfFields(view.id, 2);
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
return TestButton('Test delete a new column', testDeleteField);
|
||||
};
|
||||
|
||||
const TestButton = (title: string, onClick: () => void) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div>
|
||||
<button className='rounded-md bg-gray-300 p-4' type='button' onClick={() => onClick()}>
|
||||
{title}
|
||||
</button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
@ -2,10 +2,11 @@ import { foldersActions, IFolder } from '../../../stores/reducers/folders/slice'
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../../stores/store';
|
||||
import { IPage, pagesActions } from '../../../stores/reducers/pages/slice';
|
||||
import { ViewLayoutTypePB } from '../../../../services/backend';
|
||||
import { AppPB, ViewLayoutTypePB } from '../../../../services/backend';
|
||||
import { AppBackendService } from '../../../stores/effects/folder/app/app_bd_svc';
|
||||
import { WorkspaceBackendService } from '../../../stores/effects/folder/workspace/workspace_bd_svc';
|
||||
import { useError } from '../../error/Error.hooks';
|
||||
import { AppObserver } from '../../../stores/effects/folder/app/app_observer';
|
||||
|
||||
const initialFolderHeight = 40;
|
||||
const initialPageHeight = 40;
|
||||
@ -13,19 +14,48 @@ const animationDuration = 500;
|
||||
|
||||
export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
||||
const appDispatch = useAppDispatch();
|
||||
const workspace = useAppSelector((state) => state.workspace);
|
||||
|
||||
// Actions
|
||||
const [showPages, setShowPages] = useState(false);
|
||||
const [showFolderOptions, setShowFolderOptions] = useState(false);
|
||||
const [showNewPageOptions, setShowNewPageOptions] = useState(false);
|
||||
const [showRenamePopup, setShowRenamePopup] = useState(false);
|
||||
|
||||
// UI configurations
|
||||
const [folderHeight, setFolderHeight] = useState(`${initialFolderHeight}px`);
|
||||
|
||||
const workspace = useAppSelector((state) => state.workspace);
|
||||
// Observers
|
||||
const appObserver = new AppObserver(folder.id);
|
||||
|
||||
// Backend services
|
||||
const appBackendService = new AppBackendService(folder.id);
|
||||
const workspaceBackendService = new WorkspaceBackendService(workspace.id || '');
|
||||
|
||||
// Error
|
||||
const error = useError();
|
||||
|
||||
useEffect(() => {
|
||||
void appObserver.subscribe({
|
||||
onAppChanged: (change) => {
|
||||
if (change.ok) {
|
||||
const app: AppPB = change.val;
|
||||
const updatedPages: IPage[] = app.belongings.items.map((view) => ({
|
||||
id: view.id,
|
||||
folderId: view.app_id,
|
||||
pageType: view.layout,
|
||||
title: view.name,
|
||||
}));
|
||||
appDispatch(pagesActions.didReceivePages(updatedPages));
|
||||
}
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
// Unsubscribe when the component is unmounted.
|
||||
void appObserver.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (showPages) {
|
||||
setFolderHeight(`${initialFolderHeight + pages.length * initialPageHeight}px`);
|
||||
|
@ -69,13 +69,10 @@ export const useNavigationPanelHooks = function () {
|
||||
|
||||
return {
|
||||
width,
|
||||
|
||||
folders,
|
||||
pages,
|
||||
|
||||
navigate,
|
||||
onPageClick,
|
||||
|
||||
onCollapseNavigationClick,
|
||||
onFixNavigationClick,
|
||||
navigationPanelFixed,
|
||||
|
@ -1,4 +1,10 @@
|
||||
import { FieldType, ViewLayoutTypePB, ViewPB, WorkspaceSettingPB } from '../../../services/backend';
|
||||
import {
|
||||
FieldType,
|
||||
SingleSelectTypeOptionPB,
|
||||
ViewLayoutTypePB,
|
||||
ViewPB,
|
||||
WorkspaceSettingPB,
|
||||
} from '../../../services/backend';
|
||||
import { FolderEventReadCurrentWorkspace } from '../../../services/backend/events/flowy-folder';
|
||||
import { AppBackendService } from '../../stores/effects/folder/app/app_bd_svc';
|
||||
import { DatabaseController } from '../../stores/effects/database/database_controller';
|
||||
@ -14,6 +20,10 @@ import {
|
||||
import { None, Option, Some } from 'ts-results';
|
||||
import { TypeOptionBackendService } from '../../stores/effects/database/field/type_option/type_option_bd_svc';
|
||||
import { DatabaseBackendService } from '../../stores/effects/database/database_bd_svc';
|
||||
import { FieldInfo } from '../../stores/effects/database/field/field_controller';
|
||||
import { TypeOptionController } from '../../stores/effects/database/field/type_option/type_option_controller';
|
||||
import { makeSingleSelectTypeOptionContext } from '../../stores/effects/database/field/type_option/type_option_context';
|
||||
import { SelectOptionBackendService } from '../../stores/effects/database/cell/select_option_bd_svc';
|
||||
|
||||
// Create a database view for specific layout type
|
||||
// Do not use it production code. Just for testing
|
||||
@ -168,3 +178,36 @@ export async function assertNumberOfRows(viewId: string, expected: number) {
|
||||
throw Error('Expect number of rows:' + expected + 'but receive:' + databasePB.rows.length);
|
||||
}
|
||||
}
|
||||
|
||||
export async function assertNumberOfRowsInGroup(viewId: string, groupId: string, expected: number) {
|
||||
const svc = new DatabaseBackendService(viewId);
|
||||
await svc.openDatabase();
|
||||
|
||||
const group = await svc.getGroup(groupId).then((result) => result.unwrap());
|
||||
if (group.rows.length !== expected) {
|
||||
throw Error('Expect number of rows in group:' + expected + 'but receive:' + group.rows.length);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createSingleSelectOptions(viewId: string, fieldInfo: FieldInfo, optionNames: string[]) {
|
||||
assert(fieldInfo.field.field_type === FieldType.SingleSelect, 'Only work on single select');
|
||||
const typeOptionController = new TypeOptionController(viewId, Some(fieldInfo));
|
||||
const singleSelectTypeOptionContext = makeSingleSelectTypeOptionContext(typeOptionController);
|
||||
const singleSelectTypeOptionPB: SingleSelectTypeOptionPB = await singleSelectTypeOptionContext
|
||||
.getTypeOption()
|
||||
.then((result) => result.unwrap());
|
||||
|
||||
const backendSvc = new SelectOptionBackendService(viewId, fieldInfo.field.id);
|
||||
for (const optionName of optionNames) {
|
||||
const option = await backendSvc.createOption({ name: optionName }).then((result) => result.unwrap());
|
||||
singleSelectTypeOptionPB.options.splice(0, 0, option);
|
||||
}
|
||||
await singleSelectTypeOptionContext.setTypeOption(singleSelectTypeOptionPB);
|
||||
return singleSelectTypeOptionContext;
|
||||
}
|
||||
|
||||
export function assert(condition: boolean, msg?: string) {
|
||||
if (!condition) {
|
||||
throw Error(msg);
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import { ViewLayoutTypePB, WorkspaceSettingPB } from '../../../services/backend';
|
||||
import { FolderEventReadCurrentWorkspace } from '../../../services/backend/events/flowy-folder';
|
||||
import { AppBackendService } from '../../stores/effects/folder/app/app_bd_svc';
|
||||
|
||||
export async function createTestDocument() {
|
||||
const workspaceSetting: WorkspaceSettingPB = await FolderEventReadCurrentWorkspace().then((result) => result.unwrap());
|
||||
const app = workspaceSetting.workspace.apps.items[0];
|
||||
const appService = new AppBackendService(app.id);
|
||||
return await appService.createView({ name: 'New Document', layoutType: ViewLayoutTypePB.Document });
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
RunAllGridTests,
|
||||
TestCreateGrid,
|
||||
TestCreateNewField,
|
||||
TestCreateRow,
|
||||
@ -12,12 +13,22 @@ import {
|
||||
TestSwitchFromMultiSelectToText,
|
||||
TestSwitchFromSingleSelectToNumber,
|
||||
} from './TestGrid';
|
||||
import {
|
||||
TestCreateKanbanBoard,
|
||||
TestCreateKanbanBoardColumn,
|
||||
TestCreateKanbanBoardRowInNoStatusGroup,
|
||||
TestAllKanbanTests,
|
||||
TestMoveKanbanBoardColumn,
|
||||
TestMoveKanbanBoardRow,
|
||||
} from './TestGroup';
|
||||
import { TestCreateDocument } from './TestDocument';
|
||||
|
||||
export const TestAPI = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ul className='m-6, space-y-2'>
|
||||
{/*<TestApiButton></TestApiButton>*/}
|
||||
{/*<tests></tests>*/}
|
||||
<RunAllGridTests></RunAllGridTests>
|
||||
<TestCreateGrid></TestCreateGrid>
|
||||
<TestCreateRow></TestCreateRow>
|
||||
<TestDeleteRow></TestDeleteRow>
|
||||
@ -29,6 +40,14 @@ export const TestAPI = () => {
|
||||
<TestDeleteField></TestDeleteField>
|
||||
<TestSwitchFromSingleSelectToNumber></TestSwitchFromSingleSelectToNumber>
|
||||
<TestSwitchFromMultiSelectToText></TestSwitchFromMultiSelectToText>
|
||||
{/*kanban board */}
|
||||
<TestAllKanbanTests></TestAllKanbanTests>
|
||||
<TestCreateKanbanBoard></TestCreateKanbanBoard>
|
||||
<TestCreateKanbanBoardRowInNoStatusGroup></TestCreateKanbanBoardRowInNoStatusGroup>
|
||||
<TestMoveKanbanBoardRow></TestMoveKanbanBoardRow>
|
||||
<TestMoveKanbanBoardColumn></TestMoveKanbanBoardColumn>
|
||||
<TestCreateKanbanBoardColumn></TestCreateKanbanBoardColumn>
|
||||
<TestCreateDocument></TestCreateDocument>
|
||||
</ul>
|
||||
</React.Fragment>
|
||||
);
|
@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { createTestDocument } from './DocumentTestHelper';
|
||||
import { DocumentBackendService } from '../../stores/effects/document/document_bd_svc';
|
||||
|
||||
async function testCreateDocument() {
|
||||
const view = await createTestDocument();
|
||||
const svc = new DocumentBackendService(view.id);
|
||||
const document = await svc.open().then((result) => result.unwrap());
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const content = JSON.parse(document.content);
|
||||
// The initial document content:
|
||||
// {
|
||||
// "document": {
|
||||
// "type": "editor",
|
||||
// "children": [
|
||||
// {
|
||||
// "type": "text"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
await svc.close();
|
||||
}
|
||||
|
||||
export const TestCreateDocument = () => {
|
||||
return TestButton('Test create document', testCreateDocument);
|
||||
};
|
||||
|
||||
const TestButton = (title: string, onClick: () => void) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div>
|
||||
<button className='rounded-md bg-purple-400 p-4' type='button' onClick={() => onClick()}>
|
||||
{title}
|
||||
</button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
@ -0,0 +1,349 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
FieldType,
|
||||
NumberFormat,
|
||||
NumberTypeOptionPB,
|
||||
SelectOptionCellDataPB,
|
||||
ViewLayoutTypePB,
|
||||
} from '../../../services/backend';
|
||||
import { Log } from '../../utils/log';
|
||||
import {
|
||||
assertFieldName,
|
||||
assertNumberOfFields,
|
||||
assertNumberOfRows,
|
||||
assertTextCell,
|
||||
createSingleSelectOptions,
|
||||
createTestDatabaseView,
|
||||
editTextCell,
|
||||
findFirstFieldInfoWithFieldType,
|
||||
makeMultiSelectCellController,
|
||||
makeSingleSelectCellController,
|
||||
makeTextCellController,
|
||||
openTestDatabase,
|
||||
} from './DatabaseTestHelper';
|
||||
import { SelectOptionCellBackendService } from '../../stores/effects/database/cell/select_option_bd_svc';
|
||||
import { TypeOptionController } from '../../stores/effects/database/field/type_option/type_option_controller';
|
||||
import { None, Some } from 'ts-results';
|
||||
import { RowBackendService } from '../../stores/effects/database/row/row_bd_svc';
|
||||
import { makeNumberTypeOptionContext } from '../../stores/effects/database/field/type_option/type_option_context';
|
||||
|
||||
export const RunAllGridTests = () => {
|
||||
async function run() {
|
||||
await createBuildInGrid();
|
||||
await testEditGridRow();
|
||||
await testCreateRow();
|
||||
await testDeleteRow();
|
||||
await testCreateOptionInCell();
|
||||
await testGetSingleSelectFieldData();
|
||||
await testSwitchFromSingleSelectToNumber();
|
||||
await testSwitchFromMultiSelectToRichText();
|
||||
await testEditField();
|
||||
await testCreateNewField();
|
||||
await testDeleteField();
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div>
|
||||
<button className='rounded-md bg-red-400 p-4' type='button' onClick={() => run()}>
|
||||
Run all grid tests
|
||||
</button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
async function createBuildInGrid() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
databaseController.subscribe({
|
||||
onViewChanged: (databasePB) => {
|
||||
Log.debug('Did receive database:' + databasePB);
|
||||
},
|
||||
// onRowsChanged: async (rows) => {
|
||||
// if (rows.length !== 3) {
|
||||
// throw Error('Expected number of rows is 3, but receive ' + rows.length);
|
||||
// }
|
||||
// },
|
||||
onFieldsChanged: (fields) => {
|
||||
if (fields.length !== 3) {
|
||||
throw Error('Expected number of fields is 3, but receive ' + fields.length);
|
||||
}
|
||||
},
|
||||
});
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
async function testEditGridRow() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
|
||||
for (const [index, row] of databaseController.databaseViewCache.rowInfos.entries()) {
|
||||
const cellContent = index.toString();
|
||||
const fieldInfo = findFirstFieldInfoWithFieldType(row, FieldType.RichText).unwrap();
|
||||
await editTextCell(fieldInfo.field.id, row, databaseController, cellContent);
|
||||
await assertTextCell(fieldInfo.field.id, row, databaseController, cellContent);
|
||||
}
|
||||
}
|
||||
|
||||
async function testCreateRow() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
await assertNumberOfRows(view.id, 3);
|
||||
|
||||
// Create a row from a DatabaseController or create using the RowBackendService
|
||||
await databaseController.createRow();
|
||||
await assertNumberOfRows(view.id, 4);
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
async function testDeleteRow() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
|
||||
const rows = databaseController.databaseViewCache.rowInfos;
|
||||
const svc = new RowBackendService(view.id);
|
||||
await svc.deleteRow(rows[0].row.id);
|
||||
await assertNumberOfRows(view.id, 2);
|
||||
|
||||
// Wait the databaseViewCache get the change notification and
|
||||
// update the rows.
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
if (databaseController.databaseViewCache.rowInfos.length !== 2) {
|
||||
throw Error('The number of rows is not match');
|
||||
}
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
async function testCreateOptionInCell() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
for (const [index, row] of databaseController.databaseViewCache.rowInfos.entries()) {
|
||||
if (index === 0) {
|
||||
const fieldInfo = findFirstFieldInfoWithFieldType(row, FieldType.SingleSelect).unwrap();
|
||||
const cellController = await makeSingleSelectCellController(fieldInfo.field.id, row, databaseController).then(
|
||||
(result) => result.unwrap()
|
||||
);
|
||||
await cellController.subscribeChanged({
|
||||
onCellChanged: (value) => {
|
||||
if (value.some) {
|
||||
const option: SelectOptionCellDataPB = value.unwrap();
|
||||
console.log(option);
|
||||
}
|
||||
},
|
||||
});
|
||||
const backendSvc = new SelectOptionCellBackendService(cellController.cellIdentifier);
|
||||
await backendSvc.createOption({ name: 'option' + index });
|
||||
await cellController.dispose();
|
||||
}
|
||||
}
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
async function testGetSingleSelectFieldData() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
|
||||
// Find the single select column
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const singleSelect = databaseController.fieldController.fieldInfos.find(
|
||||
(fieldInfo) => fieldInfo.field.field_type === FieldType.SingleSelect
|
||||
)!;
|
||||
|
||||
// Create options
|
||||
const singleSelectTypeOptionContext = await createSingleSelectOptions(view.id, singleSelect, [
|
||||
'Task 1',
|
||||
'Task 2',
|
||||
'Task 3',
|
||||
]);
|
||||
|
||||
// Read options
|
||||
const options = await singleSelectTypeOptionContext.getTypeOption().then((result) => result.unwrap());
|
||||
console.log(options);
|
||||
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
async function testSwitchFromSingleSelectToNumber() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
|
||||
// Find the single select column
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const singleSelect = databaseController.fieldController.fieldInfos.find(
|
||||
(fieldInfo) => fieldInfo.field.field_type === FieldType.SingleSelect
|
||||
)!;
|
||||
const typeOptionController = new TypeOptionController(view.id, Some(singleSelect));
|
||||
await typeOptionController.switchToField(FieldType.Number);
|
||||
|
||||
// Check the number type option
|
||||
const numberTypeOptionContext = makeNumberTypeOptionContext(typeOptionController);
|
||||
const numberTypeOption: NumberTypeOptionPB = await numberTypeOptionContext
|
||||
.getTypeOption()
|
||||
.then((result) => result.unwrap());
|
||||
const format: NumberFormat = numberTypeOption.format;
|
||||
if (format !== NumberFormat.Num) {
|
||||
throw Error('The default format should be number');
|
||||
}
|
||||
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
async function testSwitchFromMultiSelectToRichText() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
|
||||
// Create multi-select field
|
||||
const typeOptionController = new TypeOptionController(view.id, None, FieldType.MultiSelect);
|
||||
await typeOptionController.initialize();
|
||||
|
||||
// Insert options to first row
|
||||
const row = databaseController.databaseViewCache.rowInfos[0];
|
||||
const multiSelectField = typeOptionController.getFieldInfo();
|
||||
// const multiSelectField = findFirstFieldInfoWithFieldType(row, FieldType.MultiSelect).unwrap();
|
||||
const selectOptionCellController = await makeMultiSelectCellController(
|
||||
multiSelectField.field.id,
|
||||
row,
|
||||
databaseController
|
||||
).then((result) => result.unwrap());
|
||||
const backendSvc = new SelectOptionCellBackendService(selectOptionCellController.cellIdentifier);
|
||||
await backendSvc.createOption({ name: 'A' });
|
||||
await backendSvc.createOption({ name: 'B' });
|
||||
await backendSvc.createOption({ name: 'C' });
|
||||
|
||||
const selectOptionCellData = await selectOptionCellController.getCellData().then((result) => result.unwrap());
|
||||
if (selectOptionCellData.options.length !== 3) {
|
||||
throw Error('The options should equal to 3');
|
||||
}
|
||||
|
||||
if (selectOptionCellData.select_options.length !== 3) {
|
||||
throw Error('The selected options should equal to 3');
|
||||
}
|
||||
await selectOptionCellController.dispose();
|
||||
|
||||
// Switch to RichText field type
|
||||
await typeOptionController.switchToField(FieldType.RichText).then((result) => result.unwrap());
|
||||
if (typeOptionController.fieldType !== FieldType.RichText) {
|
||||
throw Error('The field type should be text');
|
||||
}
|
||||
|
||||
const textCellController = await makeTextCellController(multiSelectField.field.id, row, databaseController).then(
|
||||
(result) => result.unwrap()
|
||||
);
|
||||
const cellContent = await textCellController.getCellData();
|
||||
if (cellContent.unwrap() !== 'A,B,C') {
|
||||
throw Error('The cell content should be A,B,C, but receive: ' + cellContent.unwrap());
|
||||
}
|
||||
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
async function testEditField() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
const fieldInfos = databaseController.fieldController.fieldInfos;
|
||||
|
||||
// Modify the name of the field
|
||||
const firstFieldInfo = fieldInfos[0];
|
||||
const controller = new TypeOptionController(view.id, Some(firstFieldInfo));
|
||||
await controller.initialize();
|
||||
const newName = 'hello world';
|
||||
await controller.setFieldName(newName);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
await assertFieldName(view.id, firstFieldInfo.field.id, firstFieldInfo.field.field_type, newName);
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
async function testCreateNewField() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
await assertNumberOfFields(view.id, 3);
|
||||
|
||||
// Modify the name of the field
|
||||
const controller = new TypeOptionController(view.id, None);
|
||||
await controller.initialize();
|
||||
await assertNumberOfFields(view.id, 4);
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
async function testDeleteField() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Grid);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
|
||||
// Modify the name of the field.
|
||||
// The fieldInfos[0] is the primary field by default, we can't delete it.
|
||||
// So let choose the second fieldInfo.
|
||||
const fieldInfo = databaseController.fieldController.fieldInfos[1];
|
||||
const controller = new TypeOptionController(view.id, Some(fieldInfo));
|
||||
await controller.initialize();
|
||||
await assertNumberOfFields(view.id, 3);
|
||||
await controller.deleteField();
|
||||
await assertNumberOfFields(view.id, 2);
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
export const TestCreateGrid = () => {
|
||||
return TestButton('Test create build-in grid', createBuildInGrid);
|
||||
};
|
||||
|
||||
export const TestEditCell = () => {
|
||||
return TestButton('Test editing cell', testEditGridRow);
|
||||
};
|
||||
|
||||
export const TestCreateRow = () => {
|
||||
return TestButton('Test create row', testCreateRow);
|
||||
};
|
||||
export const TestDeleteRow = () => {
|
||||
return TestButton('Test delete row', testDeleteRow);
|
||||
};
|
||||
export const TestCreateSelectOptionInCell = () => {
|
||||
return TestButton('Test create a select option in cell', testCreateOptionInCell);
|
||||
};
|
||||
|
||||
export const TestGetSingleSelectFieldData = () => {
|
||||
return TestButton('Test get single-select column data', testGetSingleSelectFieldData);
|
||||
};
|
||||
|
||||
export const TestSwitchFromSingleSelectToNumber = () => {
|
||||
return TestButton('Test switch from single-select to number column', testSwitchFromSingleSelectToNumber);
|
||||
};
|
||||
|
||||
export const TestSwitchFromMultiSelectToText = () => {
|
||||
return TestButton('Test switch from multi-select to text column', testSwitchFromMultiSelectToRichText);
|
||||
};
|
||||
|
||||
export const TestEditField = () => {
|
||||
return TestButton('Test edit the column name', testEditField);
|
||||
};
|
||||
|
||||
export const TestCreateNewField = () => {
|
||||
return TestButton('Test create a new column', testCreateNewField);
|
||||
};
|
||||
|
||||
export const TestDeleteField = () => {
|
||||
return TestButton('Test delete a new column', testDeleteField);
|
||||
};
|
||||
|
||||
export const TestButton = (title: string, onClick: () => void) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div>
|
||||
<button className='rounded-md bg-blue-400 p-4' type='button' onClick={() => onClick()}>
|
||||
{title}
|
||||
</button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
@ -0,0 +1,150 @@
|
||||
import {
|
||||
assert,
|
||||
assertNumberOfRowsInGroup,
|
||||
createSingleSelectOptions,
|
||||
createTestDatabaseView,
|
||||
openTestDatabase,
|
||||
} from './DatabaseTestHelper';
|
||||
import { FieldType, ViewLayoutTypePB } from '../../../services/backend';
|
||||
import React from 'react';
|
||||
|
||||
export const TestAllKanbanTests = () => {
|
||||
async function run() {
|
||||
await createBuildInBoard();
|
||||
await createKanbanBoardRow();
|
||||
await moveKanbanBoardRow();
|
||||
await createKanbanBoardColumn();
|
||||
await createColumnInBoard();
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div>
|
||||
<button className='rounded-md bg-red-400 p-4' type='button' onClick={() => run()}>
|
||||
Run all kanban board tests
|
||||
</button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
async function createBuildInBoard() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Board);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
databaseController.subscribe({
|
||||
onGroupByField: (groups) => {
|
||||
console.log(groups);
|
||||
if (groups.length !== 4) {
|
||||
throw Error('The build-in board should have 4 groups');
|
||||
}
|
||||
|
||||
assert(groups[0].rows.length === 0, 'The no status group should have 0 rows');
|
||||
assert(groups[1].rows.length === 3, 'The first group should have 3 rows');
|
||||
assert(groups[2].rows.length === 0, 'The second group should have 0 rows');
|
||||
assert(groups[3].rows.length === 0, 'The third group should have 0 rows');
|
||||
},
|
||||
});
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
async function createKanbanBoardRow() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Board);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
|
||||
// Create row in no status group
|
||||
const noStatusGroup = databaseController.groups.getValue()[0];
|
||||
await noStatusGroup.createRow().then((result) => result.unwrap());
|
||||
await assertNumberOfRowsInGroup(view.id, noStatusGroup.groupId, 1);
|
||||
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
async function moveKanbanBoardRow() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Board);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
|
||||
// Create row in no status group
|
||||
const firstGroup = databaseController.groups.getValue()[1];
|
||||
const secondGroup = databaseController.groups.getValue()[2];
|
||||
const row = firstGroup.rowAtIndex(0).unwrap();
|
||||
await databaseController.moveRow(row.id, secondGroup.groupId);
|
||||
|
||||
assert(firstGroup.rows.length === 2);
|
||||
await assertNumberOfRowsInGroup(view.id, firstGroup.groupId, 2);
|
||||
|
||||
assert(secondGroup.rows.length === 1);
|
||||
await assertNumberOfRowsInGroup(view.id, secondGroup.groupId, 1);
|
||||
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
async function createKanbanBoardColumn() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Board);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
|
||||
// Create row in no status group
|
||||
const firstGroup = databaseController.groups.getValue()[1];
|
||||
const secondGroup = databaseController.groups.getValue()[2];
|
||||
await databaseController.moveGroup(firstGroup.groupId, secondGroup.groupId);
|
||||
|
||||
assert(databaseController.groups.getValue()[1].groupId === secondGroup.groupId);
|
||||
assert(databaseController.groups.getValue()[2].groupId === firstGroup.groupId);
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
async function createColumnInBoard() {
|
||||
const view = await createTestDatabaseView(ViewLayoutTypePB.Board);
|
||||
const databaseController = await openTestDatabase(view.id);
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const singleSelect = databaseController.fieldController.fieldInfos.find(
|
||||
(fieldInfo) => fieldInfo.field.field_type === FieldType.SingleSelect
|
||||
)!;
|
||||
|
||||
// Create a option which will cause creating a new group
|
||||
const name = 'New column';
|
||||
await createSingleSelectOptions(view.id, singleSelect, [name]);
|
||||
|
||||
// Wait the backend posting the notification to update the groups
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
assert(databaseController.groups.value.length === 5, 'expect number of groups is 5');
|
||||
assert(databaseController.groups.value[4].name === name, 'expect the last group name is ' + name);
|
||||
await databaseController.dispose();
|
||||
}
|
||||
|
||||
export const TestCreateKanbanBoard = () => {
|
||||
return TestButton('Test create build-in board', createBuildInBoard);
|
||||
};
|
||||
|
||||
export const TestCreateKanbanBoardRowInNoStatusGroup = () => {
|
||||
return TestButton('Test create row in build-in kanban board', createKanbanBoardRow);
|
||||
};
|
||||
|
||||
export const TestMoveKanbanBoardRow = () => {
|
||||
return TestButton('Test move row in build-in kanban board', moveKanbanBoardRow);
|
||||
};
|
||||
|
||||
export const TestMoveKanbanBoardColumn = () => {
|
||||
return TestButton('Test move column in build-in kanban board', createKanbanBoardColumn);
|
||||
};
|
||||
|
||||
export const TestCreateKanbanBoardColumn = () => {
|
||||
return TestButton('Test create column in build-in kanban board', createColumnInBoard);
|
||||
};
|
||||
|
||||
export const TestButton = (title: string, onClick: () => void) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div>
|
||||
<button className='rounded-md bg-yellow-200 p-4' type='button' onClick={() => onClick()}>
|
||||
{title}
|
||||
</button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
@ -1,7 +1,16 @@
|
||||
import {
|
||||
CreateBoardCardPayloadPB,
|
||||
DatabaseEventCreateBoardCard,
|
||||
DatabaseEventCreateRow,
|
||||
DatabaseEventGetDatabase,
|
||||
DatabaseEventGetFields,
|
||||
DatabaseEventGetGroup,
|
||||
DatabaseEventGetGroups,
|
||||
DatabaseEventMoveGroup,
|
||||
DatabaseEventMoveGroupRow,
|
||||
DatabaseGroupIdPB,
|
||||
MoveGroupPayloadPB,
|
||||
MoveGroupRowPayloadPB,
|
||||
} from '../../../../services/backend/events/flowy-database';
|
||||
import {
|
||||
GetFieldPayloadPB,
|
||||
@ -37,6 +46,31 @@ export class DatabaseBackendService {
|
||||
return DatabaseEventCreateRow(payload);
|
||||
};
|
||||
|
||||
createGroupRow = async (groupId: string, startRowId?: string) => {
|
||||
const payload = CreateBoardCardPayloadPB.fromObject({ view_id: this.viewId, group_id: groupId });
|
||||
if (startRowId !== undefined) {
|
||||
payload.start_row_id = startRowId;
|
||||
}
|
||||
return DatabaseEventCreateBoardCard(payload);
|
||||
};
|
||||
|
||||
moveRow = (rowId: string, groupId?: string) => {
|
||||
const payload = MoveGroupRowPayloadPB.fromObject({ view_id: this.viewId, from_row_id: rowId });
|
||||
if (groupId !== undefined) {
|
||||
payload.to_group_id = groupId;
|
||||
}
|
||||
return DatabaseEventMoveGroupRow(payload);
|
||||
};
|
||||
|
||||
moveGroup = (fromGroupId: string, toGroupId: string) => {
|
||||
const payload = MoveGroupPayloadPB.fromObject({
|
||||
view_id: this.viewId,
|
||||
from_group_id: fromGroupId,
|
||||
to_group_id: toGroupId,
|
||||
});
|
||||
return DatabaseEventMoveGroup(payload);
|
||||
};
|
||||
|
||||
getFields = async (fieldIds?: FieldIdPB[]) => {
|
||||
const payload = GetFieldPayloadPB.fromObject({ view_id: this.viewId });
|
||||
|
||||
@ -46,4 +80,14 @@ export class DatabaseBackendService {
|
||||
|
||||
return DatabaseEventGetFields(payload).then((result) => result.map((value) => value.items));
|
||||
};
|
||||
|
||||
getGroup = (groupId: string) => {
|
||||
const payload = DatabaseGroupIdPB.fromObject({ view_id: this.viewId, group_id: groupId });
|
||||
return DatabaseEventGetGroup(payload);
|
||||
};
|
||||
|
||||
loadGroups = () => {
|
||||
const payload = DatabaseViewIdPB.fromObject({ value: this.viewId });
|
||||
return DatabaseEventGetGroups(payload);
|
||||
};
|
||||
}
|
||||
|
@ -1,55 +1,150 @@
|
||||
import { DatabaseBackendService } from './database_bd_svc';
|
||||
import { FieldController, FieldInfo } from './field/field_controller';
|
||||
import { DatabaseViewCache } from './view/database_view_cache';
|
||||
import { DatabasePB } from '../../../../services/backend';
|
||||
import { DatabasePB, GroupPB } from '../../../../services/backend';
|
||||
import { RowChangedReason, RowInfo } from './row/row_cache';
|
||||
import { Err, Ok, Result } from 'ts-results';
|
||||
import { Err } from 'ts-results';
|
||||
import { DatabaseGroupController } from './group/group_controller';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { DatabaseGroupObserver } from './group/group_observer';
|
||||
import { Log } from '../../../utils/log';
|
||||
|
||||
export type SubscribeCallbacks = {
|
||||
export type DatabaseSubscriberCallbacks = {
|
||||
onViewChanged?: (data: DatabasePB) => void;
|
||||
onRowsChanged?: (rowInfos: readonly RowInfo[], reason: RowChangedReason) => void;
|
||||
onFieldsChanged?: (fieldInfos: readonly FieldInfo[]) => void;
|
||||
onGroupByField?: (groups: GroupPB[]) => void;
|
||||
|
||||
onNumOfGroupChanged?: {
|
||||
onUpdateGroup: (value: GroupPB[]) => void;
|
||||
onDeleteGroup: (value: GroupPB[]) => void;
|
||||
onInsertGroup: (value: GroupPB[]) => void;
|
||||
};
|
||||
};
|
||||
|
||||
export class DatabaseController {
|
||||
private backendService: DatabaseBackendService;
|
||||
private readonly backendService: DatabaseBackendService;
|
||||
fieldController: FieldController;
|
||||
databaseViewCache: DatabaseViewCache;
|
||||
private _callback?: SubscribeCallbacks;
|
||||
private _callback?: DatabaseSubscriberCallbacks;
|
||||
public groups: BehaviorSubject<DatabaseGroupController[]>;
|
||||
private groupsObserver: DatabaseGroupObserver;
|
||||
|
||||
constructor(public readonly viewId: string) {
|
||||
this.backendService = new DatabaseBackendService(viewId);
|
||||
this.fieldController = new FieldController(viewId);
|
||||
this.databaseViewCache = new DatabaseViewCache(viewId, this.fieldController);
|
||||
this.groups = new BehaviorSubject<DatabaseGroupController[]>([]);
|
||||
this.groupsObserver = new DatabaseGroupObserver(viewId);
|
||||
}
|
||||
|
||||
subscribe = (callbacks: SubscribeCallbacks) => {
|
||||
subscribe = (callbacks: DatabaseSubscriberCallbacks) => {
|
||||
this._callback = callbacks;
|
||||
this.fieldController.subscribeOnNumOfFieldsChanged(callbacks.onFieldsChanged);
|
||||
this.databaseViewCache.getRowCache().subscribeOnRowsChanged((reason) => {
|
||||
this._callback?.onRowsChanged?.(this.databaseViewCache.rowInfos, reason);
|
||||
this.fieldController.subscribe({ onNumOfFieldsChanged: callbacks.onFieldsChanged });
|
||||
this.databaseViewCache.getRowCache().subscribe({
|
||||
onRowsChanged: (reason) => {
|
||||
this._callback?.onRowsChanged?.(this.databaseViewCache.rowInfos, reason);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
open = async () => {
|
||||
const result = await this.backendService.openDatabase();
|
||||
if (result.ok) {
|
||||
const database: DatabasePB = result.val;
|
||||
this._callback?.onViewChanged?.(database);
|
||||
const openDatabaseResult = await this.backendService.openDatabase();
|
||||
if (openDatabaseResult.ok) {
|
||||
const database: DatabasePB = openDatabaseResult.val;
|
||||
await this.databaseViewCache.initialize();
|
||||
await this.fieldController.initialize();
|
||||
|
||||
// subscriptions
|
||||
await this.subscribeOnGroupsChanged();
|
||||
|
||||
// load database initial data
|
||||
await this.fieldController.loadFields(database.fields);
|
||||
await this.databaseViewCache.listenOnRowsChanged();
|
||||
await this.fieldController.listenOnFieldChanges();
|
||||
const loadGroupResult = await this.loadGroup();
|
||||
|
||||
this.databaseViewCache.initializeWithRows(database.rows);
|
||||
return Ok.EMPTY;
|
||||
|
||||
this._callback?.onViewChanged?.(database);
|
||||
return loadGroupResult;
|
||||
} else {
|
||||
return Err(result.val);
|
||||
return Err(openDatabaseResult.val);
|
||||
}
|
||||
};
|
||||
|
||||
createRow = async () => {
|
||||
createRow = () => {
|
||||
return this.backendService.createRow();
|
||||
};
|
||||
|
||||
moveRow = (rowId: string, groupId: string) => {
|
||||
return this.backendService.moveRow(rowId, groupId);
|
||||
};
|
||||
|
||||
moveGroup = (fromGroupId: string, toGroupId: string) => {
|
||||
return this.backendService.moveGroup(fromGroupId, toGroupId);
|
||||
};
|
||||
|
||||
private loadGroup = async () => {
|
||||
const result = await this.backendService.loadGroups();
|
||||
if (result.ok) {
|
||||
const groups = result.val.items;
|
||||
await this.initialGroups(groups);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
private initialGroups = async (groups: GroupPB[]) => {
|
||||
this.groups.getValue().forEach((controller) => {
|
||||
void controller.dispose();
|
||||
});
|
||||
|
||||
const controllers: DatabaseGroupController[] = [];
|
||||
for (const groupPB of groups) {
|
||||
const controller = new DatabaseGroupController(groupPB, this.backendService);
|
||||
await controller.initialize();
|
||||
controllers.push(controller);
|
||||
}
|
||||
this.groups.next(controllers);
|
||||
this.groups.value;
|
||||
};
|
||||
|
||||
private subscribeOnGroupsChanged = async () => {
|
||||
await this.groupsObserver.subscribe({
|
||||
onGroupBy: async (result) => {
|
||||
if (result.ok) {
|
||||
await this.initialGroups(result.val);
|
||||
}
|
||||
},
|
||||
onGroupChangeset: (result) => {
|
||||
if (result.err) {
|
||||
Log.error(result.val);
|
||||
return;
|
||||
}
|
||||
const changeset = result.val;
|
||||
let existControllers = [...this.groups.getValue()];
|
||||
for (const deleteId of changeset.deleted_groups) {
|
||||
existControllers = existControllers.filter((c) => c.groupId !== deleteId);
|
||||
}
|
||||
|
||||
for (const update of changeset.update_groups) {
|
||||
const index = existControllers.findIndex((c) => c.groupId === update.group_id);
|
||||
if (index !== -1) {
|
||||
existControllers[index].updateGroup(update);
|
||||
}
|
||||
}
|
||||
|
||||
for (const insert of changeset.inserted_groups) {
|
||||
const controller = new DatabaseGroupController(insert.group, this.backendService);
|
||||
if (insert.index > existControllers.length) {
|
||||
existControllers.push(controller);
|
||||
} else {
|
||||
existControllers.splice(insert.index, 0, controller);
|
||||
}
|
||||
}
|
||||
this.groups.next(existControllers);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
dispose = async () => {
|
||||
await this.backendService.closeDatabase();
|
||||
await this.fieldController.dispose();
|
||||
|
@ -6,17 +6,17 @@ import { ChangeNotifier } from '../../../../utils/change_notifier';
|
||||
|
||||
export class FieldController {
|
||||
private backendService: DatabaseBackendService;
|
||||
private numOfFieldsObserver: DatabaseFieldChangesetObserver;
|
||||
private fieldChangesetObserver: DatabaseFieldChangesetObserver;
|
||||
private numOfFieldsNotifier = new NumOfFieldsNotifier([]);
|
||||
|
||||
constructor(public readonly viewId: string) {
|
||||
this.backendService = new DatabaseBackendService(viewId);
|
||||
this.numOfFieldsObserver = new DatabaseFieldChangesetObserver(viewId);
|
||||
this.fieldChangesetObserver = new DatabaseFieldChangesetObserver(viewId);
|
||||
}
|
||||
|
||||
dispose = async () => {
|
||||
this.numOfFieldsNotifier.unsubscribe();
|
||||
await this.numOfFieldsObserver.unsubscribe();
|
||||
await this.fieldChangesetObserver.unsubscribe();
|
||||
};
|
||||
|
||||
get fieldInfos(): readonly FieldInfo[] {
|
||||
@ -36,14 +36,14 @@ export class FieldController {
|
||||
}
|
||||
};
|
||||
|
||||
subscribeOnNumOfFieldsChanged = (callback?: (fieldInfos: readonly FieldInfo[]) => void) => {
|
||||
return this.numOfFieldsNotifier.observer.subscribe((fieldInfos) => {
|
||||
callback?.(fieldInfos);
|
||||
subscribe = (callbacks: { onNumOfFieldsChanged?: (fieldInfos: readonly FieldInfo[]) => void}) => {
|
||||
this.numOfFieldsNotifier.observer.subscribe((fieldInfos) => {
|
||||
callbacks.onNumOfFieldsChanged?.(fieldInfos);
|
||||
});
|
||||
};
|
||||
|
||||
listenOnFieldChanges = async () => {
|
||||
await this.numOfFieldsObserver.subscribe({
|
||||
initialize = async () => {
|
||||
await this.fieldChangesetObserver.subscribe({
|
||||
onFieldsChanged: (result) => {
|
||||
if (result.ok) {
|
||||
const changeset = result.val;
|
||||
|
@ -3,16 +3,15 @@ import { DatabaseNotification, DatabaseFieldChangesetPB, FlowyError, FieldPB } f
|
||||
import { ChangeNotifier } from '../../../../utils/change_notifier';
|
||||
import { DatabaseNotificationObserver } from '../notifications/observer';
|
||||
|
||||
type UpdateFieldNotifiedValue = Result<DatabaseFieldChangesetPB, FlowyError>;
|
||||
export type DatabaseNotificationCallback = (value: UpdateFieldNotifiedValue) => void;
|
||||
export type FieldChangesetSubscribeCallback = (value: Result<DatabaseFieldChangesetPB, FlowyError>) => void;
|
||||
|
||||
export class DatabaseFieldChangesetObserver {
|
||||
private notifier?: ChangeNotifier<UpdateFieldNotifiedValue>;
|
||||
private notifier?: ChangeNotifier<Result<DatabaseFieldChangesetPB, FlowyError>>;
|
||||
private listener?: DatabaseNotificationObserver;
|
||||
|
||||
constructor(public readonly viewId: string) {}
|
||||
|
||||
subscribe = async (callbacks: { onFieldsChanged: DatabaseNotificationCallback }) => {
|
||||
subscribe = async (callbacks: { onFieldsChanged: FieldChangesetSubscribeCallback }) => {
|
||||
this.notifier = new ChangeNotifier();
|
||||
this.notifier?.observer.subscribe(callbacks.onFieldsChanged);
|
||||
|
||||
@ -41,16 +40,15 @@ export class DatabaseFieldChangesetObserver {
|
||||
};
|
||||
}
|
||||
|
||||
type FieldNotifiedValue = Result<FieldPB, FlowyError>;
|
||||
export type FieldNotificationCallback = (value: FieldNotifiedValue) => void;
|
||||
export type FieldSubscribeCallback = (value: Result<FieldPB, FlowyError>) => void;
|
||||
|
||||
export class DatabaseFieldObserver {
|
||||
private _notifier?: ChangeNotifier<FieldNotifiedValue>;
|
||||
private _notifier?: ChangeNotifier<Result<FieldPB, FlowyError>>;
|
||||
private _listener?: DatabaseNotificationObserver;
|
||||
|
||||
constructor(public readonly fieldId: string) {}
|
||||
|
||||
subscribe = async (callbacks: { onFieldChanged: FieldNotificationCallback }) => {
|
||||
subscribe = async (callbacks: { onFieldChanged: FieldSubscribeCallback }) => {
|
||||
this._notifier = new ChangeNotifier();
|
||||
this._notifier?.observer.subscribe(callbacks.onFieldChanged);
|
||||
|
||||
|
@ -0,0 +1,149 @@
|
||||
import {
|
||||
DatabaseNotification,
|
||||
FlowyError,
|
||||
GroupPB,
|
||||
GroupRowsNotificationPB,
|
||||
RowPB,
|
||||
} from '../../../../../services/backend';
|
||||
import { ChangeNotifier } from '../../../../utils/change_notifier';
|
||||
import { None, Ok, Option, Result, Some } from 'ts-results';
|
||||
import { DatabaseNotificationObserver } from '../notifications/observer';
|
||||
import { Log } from '../../../../utils/log';
|
||||
import { DatabaseBackendService } from '../database_bd_svc';
|
||||
|
||||
export type GroupDataCallbacks = {
|
||||
onRemoveRow: (groupId: string, rowId: string) => void;
|
||||
onInsertRow: (groupId: string, row: RowPB, index?: number) => void;
|
||||
onUpdateRow: (groupId: string, row: RowPB) => void;
|
||||
|
||||
onCreateRow: (groupId: string, row: RowPB) => void;
|
||||
};
|
||||
|
||||
export class DatabaseGroupController {
|
||||
private dataObserver: GroupDataObserver;
|
||||
private callbacks?: GroupDataCallbacks;
|
||||
|
||||
constructor(private group: GroupPB, private databaseBackendSvc: DatabaseBackendService) {
|
||||
this.dataObserver = new GroupDataObserver(group.group_id);
|
||||
}
|
||||
|
||||
get groupId() {
|
||||
return this.group.group_id;
|
||||
}
|
||||
|
||||
get rows() {
|
||||
return this.group.rows;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.group.desc;
|
||||
}
|
||||
|
||||
updateGroup = (group: GroupPB) => {
|
||||
this.group = group;
|
||||
};
|
||||
|
||||
rowAtIndex = (index: number): Option<RowPB> => {
|
||||
if (this.group.rows.length < index) {
|
||||
return None;
|
||||
}
|
||||
return Some(this.group.rows[index]);
|
||||
};
|
||||
|
||||
initialize = async () => {
|
||||
await this.dataObserver.subscribe({
|
||||
onRowsChanged: (result) => {
|
||||
if (result.ok) {
|
||||
const changeset = result.val;
|
||||
// Delete
|
||||
changeset.deleted_rows.forEach((deletedRowId) => {
|
||||
this.group.rows = this.group.rows.filter((row) => row.id !== deletedRowId);
|
||||
this.callbacks?.onRemoveRow(this.group.group_id, deletedRowId);
|
||||
});
|
||||
|
||||
// Insert
|
||||
changeset.inserted_rows.forEach((insertedRow) => {
|
||||
let index: number | undefined = insertedRow.index;
|
||||
if (insertedRow.has_index && this.group.rows.length > insertedRow.index) {
|
||||
this.group.rows.splice(index, 0, insertedRow.row);
|
||||
} else {
|
||||
index = undefined;
|
||||
this.group.rows.push(insertedRow.row);
|
||||
}
|
||||
|
||||
if (insertedRow.is_new) {
|
||||
this.callbacks?.onCreateRow(this.group.group_id, insertedRow.row);
|
||||
} else {
|
||||
this.callbacks?.onInsertRow(this.group.group_id, insertedRow.row, index);
|
||||
}
|
||||
});
|
||||
|
||||
// Update
|
||||
changeset.updated_rows.forEach((updatedRow) => {
|
||||
const index = this.group.rows.findIndex((row) => row.id === updatedRow.id);
|
||||
if (index !== -1) {
|
||||
this.group.rows[index] = updatedRow;
|
||||
this.callbacks?.onUpdateRow(this.group.group_id, updatedRow);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Log.error(result.val);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
createRow = async () => {
|
||||
return this.databaseBackendSvc.createGroupRow(this.group.group_id);
|
||||
};
|
||||
|
||||
subscribe = (callbacks: GroupDataCallbacks) => {
|
||||
this.callbacks = callbacks;
|
||||
};
|
||||
|
||||
unsubscribe = () => {
|
||||
this.callbacks = undefined;
|
||||
};
|
||||
|
||||
dispose = async () => {
|
||||
await this.dataObserver.unsubscribe();
|
||||
this.callbacks = undefined;
|
||||
};
|
||||
}
|
||||
|
||||
type GroupRowsSubscribeCallback = (value: Result<GroupRowsNotificationPB, FlowyError>) => void;
|
||||
|
||||
class GroupDataObserver {
|
||||
private notifier?: ChangeNotifier<Result<GroupRowsNotificationPB, FlowyError>>;
|
||||
private listener?: DatabaseNotificationObserver;
|
||||
|
||||
constructor(public readonly groupId: string) {}
|
||||
|
||||
subscribe = async (callbacks: { onRowsChanged: GroupRowsSubscribeCallback }) => {
|
||||
this.notifier = new ChangeNotifier();
|
||||
this.notifier?.observer.subscribe(callbacks.onRowsChanged);
|
||||
|
||||
this.listener = new DatabaseNotificationObserver({
|
||||
id: this.groupId,
|
||||
parserHandler: (notification, result) => {
|
||||
switch (notification) {
|
||||
case DatabaseNotification.DidUpdateGroupRow:
|
||||
if (result.ok) {
|
||||
this.notifier?.notify(Ok(GroupRowsNotificationPB.deserializeBinary(result.val)));
|
||||
} else {
|
||||
this.notifier?.notify(result);
|
||||
}
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
await this.listener.start();
|
||||
};
|
||||
|
||||
unsubscribe = async () => {
|
||||
await this.listener?.stop();
|
||||
this.notifier?.unsubscribe();
|
||||
};
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
import { ChangeNotifier } from '../../../../utils/change_notifier';
|
||||
import { Ok, Result } from 'ts-results';
|
||||
import { DatabaseNotification, FlowyError, GroupChangesetPB, GroupPB } from '../../../../../services/backend';
|
||||
import { DatabaseNotificationObserver } from '../notifications/observer';
|
||||
|
||||
export type GroupByFieldCallback = (value: Result<GroupPB[], FlowyError>) => void;
|
||||
export type GroupChangesetSubscribeCallback = (value: Result<GroupChangesetPB, FlowyError>) => void;
|
||||
|
||||
export class DatabaseGroupObserver {
|
||||
private groupByNotifier?: ChangeNotifier<Result<GroupPB[], FlowyError>>;
|
||||
private groupChangesetNotifier?: ChangeNotifier<Result<GroupChangesetPB, FlowyError>>;
|
||||
private listener?: DatabaseNotificationObserver;
|
||||
|
||||
constructor(public readonly viewId: string) {}
|
||||
|
||||
subscribe = async (callbacks: {
|
||||
onGroupBy: GroupByFieldCallback;
|
||||
onGroupChangeset: GroupChangesetSubscribeCallback;
|
||||
}) => {
|
||||
this.groupByNotifier = new ChangeNotifier();
|
||||
this.groupByNotifier?.observer.subscribe(callbacks.onGroupBy);
|
||||
|
||||
this.groupChangesetNotifier = new ChangeNotifier();
|
||||
this.groupChangesetNotifier?.observer.subscribe(callbacks.onGroupChangeset);
|
||||
|
||||
this.listener = new DatabaseNotificationObserver({
|
||||
id: this.viewId,
|
||||
parserHandler: (notification, result) => {
|
||||
switch (notification) {
|
||||
case DatabaseNotification.DidGroupByField:
|
||||
if (result.ok) {
|
||||
this.groupByNotifier?.notify(Ok(GroupChangesetPB.deserializeBinary(result.val).initial_groups));
|
||||
} else {
|
||||
this.groupByNotifier?.notify(result);
|
||||
}
|
||||
break;
|
||||
case DatabaseNotification.DidUpdateGroups:
|
||||
if (result.ok) {
|
||||
this.groupChangesetNotifier?.notify(Ok(GroupChangesetPB.deserializeBinary(result.val)));
|
||||
} else {
|
||||
this.groupChangesetNotifier?.notify(result);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await this.listener.start();
|
||||
};
|
||||
|
||||
unsubscribe = async () => {
|
||||
this.groupByNotifier?.unsubscribe();
|
||||
this.groupChangesetNotifier?.unsubscribe();
|
||||
await this.listener?.stop();
|
||||
};
|
||||
}
|
@ -53,12 +53,14 @@ export class RowCache {
|
||||
}
|
||||
};
|
||||
|
||||
subscribeOnRowsChanged = (callback: (reason: RowChangedReason, cellMap?: Map<string, CellIdentifier>) => void) => {
|
||||
subscribe = (callbacks: {
|
||||
onRowsChanged: (reason: RowChangedReason, cellMap?: Map<string, CellIdentifier>) => void;
|
||||
}) => {
|
||||
return this.notifier.observer.subscribe((change) => {
|
||||
if (change.rowId !== undefined) {
|
||||
callback(change.reason, this._toCellMap(change.rowId, this.getFieldInfos()));
|
||||
callbacks.onRowsChanged(change.reason, this._toCellMap(change.rowId, this.getFieldInfos()));
|
||||
} else {
|
||||
callback(change.reason);
|
||||
callbacks.onRowsChanged(change.reason);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -7,16 +7,17 @@ import { Subscription } from 'rxjs';
|
||||
export class DatabaseViewCache {
|
||||
private readonly rowsObserver: DatabaseViewRowsObserver;
|
||||
private readonly rowCache: RowCache;
|
||||
private readonly fieldSubscription?: Subscription;
|
||||
|
||||
constructor(public readonly viewId: string, fieldController: FieldController) {
|
||||
this.rowsObserver = new DatabaseViewRowsObserver(viewId);
|
||||
this.rowCache = new RowCache(viewId, () => fieldController.fieldInfos);
|
||||
this.fieldSubscription = fieldController.subscribeOnNumOfFieldsChanged((fieldInfos) => {
|
||||
fieldInfos.forEach((fieldInfo) => {
|
||||
this.rowCache.onFieldUpdated(fieldInfo);
|
||||
});
|
||||
this.rowCache.onNumberOfFieldsUpdated(fieldInfos);
|
||||
fieldController.subscribe({
|
||||
onNumOfFieldsChanged: (fieldInfos) => {
|
||||
fieldInfos.forEach((fieldInfo) => {
|
||||
this.rowCache.onFieldUpdated(fieldInfo);
|
||||
});
|
||||
this.rowCache.onNumberOfFieldsUpdated(fieldInfos);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -33,12 +34,11 @@ export class DatabaseViewCache {
|
||||
};
|
||||
|
||||
dispose = async () => {
|
||||
this.fieldSubscription?.unsubscribe();
|
||||
await this.rowsObserver.unsubscribe();
|
||||
await this.rowCache.dispose();
|
||||
};
|
||||
|
||||
listenOnRowsChanged = async () => {
|
||||
initialize = async () => {
|
||||
await this.rowsObserver.subscribe({
|
||||
onRowsVisibilityChanged: (result) => {
|
||||
if (result.ok) {
|
||||
|
@ -0,0 +1,30 @@
|
||||
import {
|
||||
DocumentDataPB,
|
||||
DocumentVersionPB,
|
||||
EditPayloadPB,
|
||||
FlowyError,
|
||||
OpenDocumentPayloadPB,
|
||||
ViewIdPB,
|
||||
} from '../../../../services/backend';
|
||||
import { DocumentEventApplyEdit, DocumentEventGetDocument } from '../../../../services/backend/events/flowy-document';
|
||||
import { Result } from 'ts-results';
|
||||
import { FolderEventCloseView } from '../../../../services/backend/events/flowy-folder';
|
||||
|
||||
export class DocumentBackendService {
|
||||
constructor(public readonly viewId: string) {}
|
||||
|
||||
open = (): Promise<Result<DocumentDataPB, FlowyError>> => {
|
||||
const payload = OpenDocumentPayloadPB.fromObject({ document_id: this.viewId, version: DocumentVersionPB.V1 });
|
||||
return DocumentEventGetDocument(payload);
|
||||
};
|
||||
|
||||
applyEdit = (operations: string) => {
|
||||
const payload = EditPayloadPB.fromObject({ doc_id: this.viewId, operations: operations });
|
||||
return DocumentEventApplyEdit(payload);
|
||||
};
|
||||
|
||||
close = () => {
|
||||
const payload = ViewIdPB.fromObject({ value: this.viewId });
|
||||
return FolderEventCloseView(payload);
|
||||
};
|
||||
}
|
@ -0,0 +1 @@
|
||||
export class Document {}
|
@ -3,11 +3,10 @@ import { AppPB, FlowyError, FolderNotification } from '../../../../../services/b
|
||||
import { ChangeNotifier } from '../../../../utils/change_notifier';
|
||||
import { FolderNotificationObserver } from '../notifications/observer';
|
||||
|
||||
export type AppUpdateNotifyValue = Result<AppPB, FlowyError>;
|
||||
export type AppUpdateNotifyCallback = (value: AppUpdateNotifyValue) => void;
|
||||
export type AppUpdateNotifyCallback = (value: Result<AppPB, FlowyError>) => void;
|
||||
|
||||
export class AppObserver {
|
||||
_appNotifier = new ChangeNotifier<AppUpdateNotifyValue>();
|
||||
_appNotifier = new ChangeNotifier<Result<AppPB, FlowyError>>();
|
||||
_listener?: FolderNotificationObserver;
|
||||
|
||||
constructor(public readonly appId: string) {}
|
||||
|
@ -14,6 +14,14 @@ export const pagesSlice = createSlice({
|
||||
name: 'pages',
|
||||
initialState: initialState,
|
||||
reducers: {
|
||||
didReceivePages(state, action: PayloadAction<IPage[]>) {
|
||||
action.payload.forEach((updatedPage) => {
|
||||
const index = state.findIndex((page) => page.id === updatedPage.id);
|
||||
if (index !== -1) {
|
||||
state.splice(index, 1, updatedPage);
|
||||
}
|
||||
});
|
||||
},
|
||||
addPage(state, action: PayloadAction<IPage>) {
|
||||
state.push(action.payload);
|
||||
},
|
||||
|
@ -1,4 +1,158 @@
|
||||
use crate::entities::parser::NotEmptyStr;
|
||||
use crate::entities::{FieldIdPB, RowPB};
|
||||
use flowy_derive::ProtoBuf;
|
||||
use flowy_error::ErrorCode;
|
||||
|
||||
/// [DatabasePB] describes how many fields and blocks the grid has
|
||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||
pub struct DatabasePB {
|
||||
#[pb(index = 1)]
|
||||
pub id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub fields: Vec<FieldIdPB>,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub rows: Vec<RowPB>,
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf, Default)]
|
||||
pub struct CreateDatabasePayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, ProtoBuf, Default, Debug)]
|
||||
pub struct DatabaseViewIdPB {
|
||||
#[pb(index = 1)]
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
impl AsRef<str> for DatabaseViewIdPB {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||
pub struct MoveFieldPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub view_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub field_id: String,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub from_index: i32,
|
||||
|
||||
#[pb(index = 4)]
|
||||
pub to_index: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MoveFieldParams {
|
||||
pub view_id: String,
|
||||
pub field_id: String,
|
||||
pub from_index: i32,
|
||||
pub to_index: i32,
|
||||
}
|
||||
|
||||
impl TryInto<MoveFieldParams> for MoveFieldPayloadPB {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_into(self) -> Result<MoveFieldParams, Self::Error> {
|
||||
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
|
||||
let item_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::InvalidData)?;
|
||||
Ok(MoveFieldParams {
|
||||
view_id: view_id.0,
|
||||
field_id: item_id.0,
|
||||
from_index: self.from_index,
|
||||
to_index: self.to_index,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||
pub struct MoveRowPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub view_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub from_row_id: String,
|
||||
|
||||
#[pb(index = 4)]
|
||||
pub to_row_id: String,
|
||||
}
|
||||
|
||||
pub struct MoveRowParams {
|
||||
pub view_id: String,
|
||||
pub from_row_id: String,
|
||||
pub to_row_id: String,
|
||||
}
|
||||
|
||||
impl TryInto<MoveRowParams> for MoveRowPayloadPB {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_into(self) -> Result<MoveRowParams, Self::Error> {
|
||||
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
|
||||
let from_row_id = NotEmptyStr::parse(self.from_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?;
|
||||
let to_row_id = NotEmptyStr::parse(self.to_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?;
|
||||
|
||||
Ok(MoveRowParams {
|
||||
view_id: view_id.0,
|
||||
from_row_id: from_row_id.0,
|
||||
to_row_id: to_row_id.0,
|
||||
})
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||
pub struct MoveGroupRowPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub view_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub from_row_id: String,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub to_group_id: String,
|
||||
|
||||
#[pb(index = 4, one_of)]
|
||||
pub to_row_id: Option<String>,
|
||||
}
|
||||
|
||||
pub struct MoveGroupRowParams {
|
||||
pub view_id: String,
|
||||
pub from_row_id: String,
|
||||
pub to_group_id: String,
|
||||
pub to_row_id: Option<String>,
|
||||
}
|
||||
|
||||
impl TryInto<MoveGroupRowParams> for MoveGroupRowPayloadPB {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_into(self) -> Result<MoveGroupRowParams, Self::Error> {
|
||||
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
|
||||
let from_row_id = NotEmptyStr::parse(self.from_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?;
|
||||
let to_group_id =
|
||||
NotEmptyStr::parse(self.to_group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?;
|
||||
|
||||
let to_row_id = match self.to_row_id {
|
||||
None => None,
|
||||
Some(to_row_id) => Some(
|
||||
NotEmptyStr::parse(to_row_id)
|
||||
.map_err(|_| ErrorCode::RowIdIsEmpty)?
|
||||
.0,
|
||||
),
|
||||
};
|
||||
|
||||
Ok(MoveGroupRowParams {
|
||||
view_id: view_id.0,
|
||||
from_row_id: from_row_id.0,
|
||||
to_group_id: to_group_id.0,
|
||||
to_row_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, ProtoBuf)]
|
||||
pub struct DatabaseDescPB {
|
||||
@ -14,3 +168,30 @@ pub struct RepeatedDatabaseDescPB {
|
||||
#[pb(index = 1)]
|
||||
pub items: Vec<DatabaseDescPB>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||
pub struct DatabaseGroupIdPB {
|
||||
#[pb(index = 1)]
|
||||
pub view_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub group_id: String,
|
||||
}
|
||||
|
||||
pub struct DatabaseGroupIdParams {
|
||||
pub view_id: String,
|
||||
pub group_id: String,
|
||||
}
|
||||
|
||||
impl TryInto<DatabaseGroupIdParams> for DatabaseGroupIdPB {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_into(self) -> Result<DatabaseGroupIdParams, Self::Error> {
|
||||
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
|
||||
let group_id = NotEmptyStr::parse(self.group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?;
|
||||
Ok(DatabaseGroupIdParams {
|
||||
view_id: view_id.0,
|
||||
group_id: group_id.0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,155 +0,0 @@
|
||||
use crate::entities::parser::NotEmptyStr;
|
||||
use crate::entities::{FieldIdPB, RowPB};
|
||||
use flowy_derive::ProtoBuf;
|
||||
use flowy_error::ErrorCode;
|
||||
|
||||
/// [DatabasePB] describes how many fields and blocks the grid has
|
||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||
pub struct DatabasePB {
|
||||
#[pb(index = 1)]
|
||||
pub id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub fields: Vec<FieldIdPB>,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub rows: Vec<RowPB>,
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf, Default)]
|
||||
pub struct CreateDatabasePayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, ProtoBuf, Default, Debug)]
|
||||
pub struct DatabaseViewIdPB {
|
||||
#[pb(index = 1)]
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
impl AsRef<str> for DatabaseViewIdPB {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||
pub struct MoveFieldPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub view_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub field_id: String,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub from_index: i32,
|
||||
|
||||
#[pb(index = 4)]
|
||||
pub to_index: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MoveFieldParams {
|
||||
pub view_id: String,
|
||||
pub field_id: String,
|
||||
pub from_index: i32,
|
||||
pub to_index: i32,
|
||||
}
|
||||
|
||||
impl TryInto<MoveFieldParams> for MoveFieldPayloadPB {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_into(self) -> Result<MoveFieldParams, Self::Error> {
|
||||
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
|
||||
let item_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::InvalidData)?;
|
||||
Ok(MoveFieldParams {
|
||||
view_id: view_id.0,
|
||||
field_id: item_id.0,
|
||||
from_index: self.from_index,
|
||||
to_index: self.to_index,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||
pub struct MoveRowPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub view_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub from_row_id: String,
|
||||
|
||||
#[pb(index = 4)]
|
||||
pub to_row_id: String,
|
||||
}
|
||||
|
||||
pub struct MoveRowParams {
|
||||
pub view_id: String,
|
||||
pub from_row_id: String,
|
||||
pub to_row_id: String,
|
||||
}
|
||||
|
||||
impl TryInto<MoveRowParams> for MoveRowPayloadPB {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_into(self) -> Result<MoveRowParams, Self::Error> {
|
||||
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
|
||||
let from_row_id = NotEmptyStr::parse(self.from_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?;
|
||||
let to_row_id = NotEmptyStr::parse(self.to_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?;
|
||||
|
||||
Ok(MoveRowParams {
|
||||
view_id: view_id.0,
|
||||
from_row_id: from_row_id.0,
|
||||
to_row_id: to_row_id.0,
|
||||
})
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||
pub struct MoveGroupRowPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub view_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub from_row_id: String,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub to_group_id: String,
|
||||
|
||||
#[pb(index = 4, one_of)]
|
||||
pub to_row_id: Option<String>,
|
||||
}
|
||||
|
||||
pub struct MoveGroupRowParams {
|
||||
pub view_id: String,
|
||||
pub from_row_id: String,
|
||||
pub to_group_id: String,
|
||||
pub to_row_id: Option<String>,
|
||||
}
|
||||
|
||||
impl TryInto<MoveGroupRowParams> for MoveGroupRowPayloadPB {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_into(self) -> Result<MoveGroupRowParams, Self::Error> {
|
||||
let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?;
|
||||
let from_row_id = NotEmptyStr::parse(self.from_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?;
|
||||
let to_group_id =
|
||||
NotEmptyStr::parse(self.to_group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?;
|
||||
|
||||
let to_row_id = match self.to_row_id {
|
||||
None => None,
|
||||
Some(to_row_id) => Some(
|
||||
NotEmptyStr::parse(to_row_id)
|
||||
.map_err(|_| ErrorCode::RowIdIsEmpty)?
|
||||
.0,
|
||||
),
|
||||
};
|
||||
|
||||
Ok(MoveGroupRowParams {
|
||||
view_id: view_id.0,
|
||||
from_row_id: from_row_id.0,
|
||||
to_group_id: to_group_id.0,
|
||||
to_row_id,
|
||||
})
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ mod cell_entities;
|
||||
mod database_entities;
|
||||
mod field_entities;
|
||||
pub mod filter_entities;
|
||||
mod grid_entities;
|
||||
mod group_entities;
|
||||
pub mod parser;
|
||||
mod row_entities;
|
||||
@ -14,9 +13,9 @@ mod view_entities;
|
||||
pub use calendar_entities::*;
|
||||
pub use cell_entities::*;
|
||||
pub use database_entities::*;
|
||||
pub use database_entities::*;
|
||||
pub use field_entities::*;
|
||||
pub use filter_entities::*;
|
||||
pub use grid_entities::*;
|
||||
pub use group_entities::*;
|
||||
pub use row_entities::*;
|
||||
pub use setting_entities::*;
|
||||
|
@ -538,6 +538,17 @@ pub(crate) async fn get_groups_handler(
|
||||
data_result_ok(groups)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all, err)]
|
||||
pub(crate) async fn get_group_handler(
|
||||
data: AFPluginData<DatabaseGroupIdPB>,
|
||||
manager: AFPluginState<Arc<DatabaseManager>>,
|
||||
) -> DataResult<GroupPB, FlowyError> {
|
||||
let params: DatabaseGroupIdParams = data.into_inner().try_into()?;
|
||||
let editor = manager.get_database_editor(¶ms.view_id).await?;
|
||||
let group = editor.get_group(¶ms.view_id, ¶ms.group_id).await?;
|
||||
data_result_ok(group)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(data, manager), err)]
|
||||
pub(crate) async fn create_board_card_handler(
|
||||
data: AFPluginData<CreateBoardCardPayloadPB>,
|
||||
|
@ -47,7 +47,8 @@ pub fn init(database_manager: Arc<DatabaseManager>) -> AFPlugin {
|
||||
.event(DatabaseEvent::CreateBoardCard, create_board_card_handler)
|
||||
.event(DatabaseEvent::MoveGroup, move_group_handler)
|
||||
.event(DatabaseEvent::MoveGroupRow, move_group_row_handler)
|
||||
.event(DatabaseEvent::GetGroup, get_groups_handler)
|
||||
.event(DatabaseEvent::GetGroups, get_groups_handler)
|
||||
.event(DatabaseEvent::GetGroup, get_group_handler)
|
||||
// Database
|
||||
.event(DatabaseEvent::GetDatabases, get_databases_handler)
|
||||
// Calendar
|
||||
@ -221,7 +222,10 @@ pub enum DatabaseEvent {
|
||||
UpdateDateCell = 80,
|
||||
|
||||
#[event(input = "DatabaseViewIdPB", output = "RepeatedGroupPB")]
|
||||
GetGroup = 100,
|
||||
GetGroups = 100,
|
||||
|
||||
#[event(input = "DatabaseGroupIdPB", output = "GroupPB")]
|
||||
GetGroup = 101,
|
||||
|
||||
#[event(input = "CreateBoardCardPayloadPB", output = "RowPB")]
|
||||
CreateBoardCard = 110,
|
||||
|
@ -207,14 +207,14 @@ impl DatabaseManager {
|
||||
let create_view_editor = |database_editor: Arc<DatabaseEditor>| async move {
|
||||
let user_id = user.user_id()?;
|
||||
let (view_pad, view_rev_manager) = make_database_view_revision_pad(view_id, user).await?;
|
||||
return DatabaseViewEditor::from_pad(
|
||||
DatabaseViewEditor::from_pad(
|
||||
&user_id,
|
||||
database_editor.database_view_data.clone(),
|
||||
database_editor.cell_data_cache.clone(),
|
||||
view_rev_manager,
|
||||
view_pad,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
};
|
||||
|
||||
let database_editor = self
|
||||
@ -224,7 +224,7 @@ impl DatabaseManager {
|
||||
.get(database_id)
|
||||
.cloned();
|
||||
|
||||
return match database_editor {
|
||||
match database_editor {
|
||||
None => {
|
||||
let mut editors_by_database_id = self.editors_by_database_id.write().await;
|
||||
let db_pool = self.database_user.db_pool()?;
|
||||
@ -241,7 +241,7 @@ impl DatabaseManager {
|
||||
|
||||
Ok(database_editor)
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self, pool), err)]
|
||||
|
@ -924,6 +924,11 @@ impl DatabaseEditor {
|
||||
self.database_views.load_groups(view_id).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all, err)]
|
||||
pub async fn get_group(&self, view_id: &str, group_id: &str) -> FlowyResult<GroupPB> {
|
||||
self.database_views.get_group(view_id, group_id).await
|
||||
}
|
||||
|
||||
async fn create_row_rev(&self) -> FlowyResult<RowRevision> {
|
||||
let field_revs = self.database_pad.read().await.get_field_revs(None)?;
|
||||
let block_id = self.block_id().await?;
|
||||
|
@ -24,7 +24,7 @@ use database_model::{
|
||||
use flowy_client_sync::client_database::{
|
||||
make_database_view_operations, DatabaseViewRevisionChangeset, DatabaseViewRevisionPad,
|
||||
};
|
||||
use flowy_error::FlowyResult;
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use flowy_revision::RevisionManager;
|
||||
use flowy_sqlite::ConnectionPool;
|
||||
use flowy_task::TaskDispatcher;
|
||||
@ -379,7 +379,7 @@ impl DatabaseViewEditor {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Only call once after grid view editor initialized
|
||||
/// Only call once after database view editor initialized
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
pub async fn v_load_groups(&self) -> FlowyResult<Vec<GroupPB>> {
|
||||
let groups = self
|
||||
@ -394,6 +394,14 @@ impl DatabaseViewEditor {
|
||||
Ok(groups.into_iter().map(GroupPB::from).collect())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
pub async fn v_get_group(&self, group_id: &str) -> FlowyResult<GroupPB> {
|
||||
match self.group_controller.read().await.get_group(group_id) {
|
||||
None => Err(FlowyError::record_not_found().context("Can't find the group")),
|
||||
Some((_, group)) => Ok(GroupPB::from(group)),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self), err)]
|
||||
pub async fn v_move_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
|
||||
self
|
||||
|
@ -1,7 +1,8 @@
|
||||
#![allow(clippy::while_let_loop)]
|
||||
use crate::entities::{
|
||||
AlterFilterParams, AlterSortParams, CreateRowParams, DatabaseViewSettingPB, DeleteFilterParams,
|
||||
DeleteGroupParams, DeleteSortParams, InsertGroupParams, MoveGroupParams, RepeatedGroupPB, RowPB,
|
||||
DeleteGroupParams, DeleteSortParams, GroupPB, InsertGroupParams, MoveGroupParams,
|
||||
RepeatedGroupPB, RowPB,
|
||||
};
|
||||
use crate::manager::DatabaseUser;
|
||||
use crate::services::cell::AtomicCellDataCache;
|
||||
@ -202,6 +203,11 @@ impl DatabaseViews {
|
||||
Ok(RepeatedGroupPB { items: groups })
|
||||
}
|
||||
|
||||
pub async fn get_group(&self, view_id: &str, group_id: &str) -> FlowyResult<GroupPB> {
|
||||
let view_editor = self.get_view_editor(view_id).await?;
|
||||
view_editor.v_get_group(group_id).await
|
||||
}
|
||||
|
||||
pub async fn insert_or_update_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
|
||||
let view_editor = self.get_view_editor(¶ms.view_id).await?;
|
||||
view_editor.v_initialize_new_group(params).await
|
||||
|
Loading…
Reference in New Issue
Block a user