Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3d6c66f5c | ||
|
|
d665153a9f | ||
|
|
eee32110f4 | ||
|
|
9834eccc7b | ||
|
|
bef7fe87aa | ||
|
|
feceb430cf | ||
|
|
a29c8ab27a | ||
|
|
f1bfcb6066 | ||
|
|
99cfda0a5c |
No files matched your search
@@ -21,12 +21,13 @@ on:
|
||||
- unlocked
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-app:
|
||||
if: github.event.pull_request.draft != true
|
||||
concurrency:
|
||||
group: docker_ci-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
|
||||
@@ -25,8 +25,13 @@ env:
|
||||
FLUTTER_VERSION: "3.10.1"
|
||||
RUST_TOOLCHAIN: "1.70"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -102,7 +107,6 @@ jobs:
|
||||
git config --system core.longpaths true
|
||||
flutter config --enable-windows-desktop
|
||||
fi
|
||||
dart pub global activate protoc_plugin 20.0.1
|
||||
shell: bash
|
||||
|
||||
- name: Build AppFlowy
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- ".github/workflows/flutter_ci.yaml"
|
||||
- ".github/workflows/integration_test.yml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
|
||||
@@ -15,7 +15,7 @@ on:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- ".github/workflows/flutter_ci.yaml"
|
||||
- ".github/workflows/integration_test.yml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
|
||||
@@ -24,8 +24,13 @@ env:
|
||||
FLUTTER_VERSION: "3.10.1"
|
||||
RUST_TOOLCHAIN: "1.70"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
@@ -43,7 +48,6 @@ jobs:
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
target: ${{ matrix.target }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
@@ -92,7 +96,6 @@ jobs:
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
flutter config --enable-windows-desktop
|
||||
fi
|
||||
dart pub global activate protoc_plugin 20.0.1
|
||||
shell: bash
|
||||
|
||||
- name: Build Test lib
|
||||
@@ -114,11 +117,11 @@ jobs:
|
||||
export DISPLAY=:99
|
||||
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
|
||||
sudo apt-get install network-manager
|
||||
flutter test integration_test/runner.dart -d Linux --coverage --verbose
|
||||
flutter test integration_test/runner.dart -d Linux --coverage
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
flutter test integration_test/runner.dart -d macOS --coverage --verbose
|
||||
flutter test integration_test/runner.dart -d macOS --coverage
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
flutter test integration_test/runner.dart -d Windows --coverage --verbose
|
||||
flutter test integration_test/runner.dart -d Windows --coverage
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ name: Tauri-CI
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/tauri_ci.yaml"
|
||||
- "frontend/rust-lib/**"
|
||||
- "frontend/appflowy_tauri/**"
|
||||
|
||||
@@ -10,8 +11,13 @@ env:
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.70"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tauri-build:
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -38,7 +44,7 @@ jobs:
|
||||
with:
|
||||
prefix-key: 'ubuntu-latest-tauri'
|
||||
workspaces: |
|
||||
frontend/rust-lib
|
||||
frontend/rust-lib
|
||||
frontend/appflowy_tauri/src-tauri
|
||||
|
||||
- name: install dependencies (windows only)
|
||||
|
||||
+28
-1
@@ -1,27 +1,44 @@
|
||||
# Release Notes
|
||||
|
||||
## Version 0.2.4 - 06/23/2023
|
||||
|
||||
### Bug fixes:
|
||||
|
||||
- Unable to copy and paste a word
|
||||
- Some UI issues
|
||||
|
||||
## Version 0.2.3 - 06/21/2023
|
||||
|
||||
### New Features
|
||||
- Added support for creating multiple database views for existing database
|
||||
|
||||
- Added support for creating multiple database views for existing database
|
||||
|
||||
## Version 0.2.2 - 06/15/2023
|
||||
|
||||
### New Features
|
||||
|
||||
- Added support for embedding a document in the database's row detail page
|
||||
- Added support for inserting an emoji in the database's row detail page
|
||||
|
||||
### Other Updates
|
||||
|
||||
- Added language selector on the welcome page
|
||||
- Added support for importing multiple markdown files all at once
|
||||
|
||||
## Version 0.2.1 - 06/11/2023
|
||||
|
||||
### New Features
|
||||
|
||||
- Added support for creating or referencing a calendar in the document
|
||||
- Added `+` icon in grid's add field
|
||||
|
||||
### Other Updates
|
||||
|
||||
- Added vertical padding for progress bar
|
||||
- Hide url cell accessory when the content is empty
|
||||
|
||||
### Bug fixes:
|
||||
|
||||
- Fixed unable to export markdown
|
||||
- Fixed adding vertical padding for progress bar
|
||||
- Fixed database view didn't update after the database layout changed.
|
||||
@@ -29,6 +46,7 @@
|
||||
## Version 0.2.0 - 06/08/2023
|
||||
|
||||
### New Features
|
||||
|
||||
- Improved checklists to support each cell having its own list
|
||||
- Drag and drop calendar events
|
||||
- Switch layouts (calendar, grid, kanban) of a database
|
||||
@@ -42,44 +60,53 @@
|
||||
- Added support for an 'Option' button to delete, duplicate, and customize block actions
|
||||
|
||||
### Other Updates
|
||||
|
||||
- Added support for importing v0.1.x documents and databases
|
||||
- Added support for database import and export to CSV
|
||||
- Optimized scroll behavior in documents.
|
||||
- Redesigned the launch page
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fixed bugs related to numbers
|
||||
- Fixed issues with referenced databases in documents
|
||||
- Fixed menu overflow issues in documents
|
||||
|
||||
### Data migration
|
||||
|
||||
The data format of this version is not compatible with previous versions. Therefore, to migrate your data to the new version, you need to use the export and import functions. Please follow the guide to learn how to export and import your data.
|
||||
|
||||
#### Export files in v0.1.6
|
||||
|
||||
https://github.com/AppFlowy-IO/AppFlowy/assets/11863087/0c89bf2b-cd97-4a7b-b627-59df8d2967d9
|
||||
|
||||
#### Import files in v0.2.0
|
||||
|
||||
https://github.com/AppFlowy-IO/AppFlowy/assets/11863087/7b392f35-4972-497a-8a7f-f38efced32e2
|
||||
|
||||
## Version 0.1.5 - 11/05/2023
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: calendar dates don't match with weekdays.
|
||||
- Fix: sort numbers in Grid.
|
||||
|
||||
## Version 0.1.4 - 04/05/2023
|
||||
|
||||
### New features
|
||||
|
||||
- Use AppFlowy’s calendar views to plan and manage tasks and deadlines.
|
||||
- Writing can be improved with the help of OpenAI.
|
||||
|
||||
## Version 0.1.3 - 24/04/2023
|
||||
|
||||
### New features
|
||||
|
||||
- Launch the official Dark Mode.
|
||||
- Customize the font color and highlight color by setting a hex color value and an opacity level.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: the slash menu can be triggered by all other keyboards than English.
|
||||
- Fix: convert the single asterisk to italic text and the double asterisks to bold text.
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
CURRENT_APP_VERSION = "0.2.3"
|
||||
CURRENT_APP_VERSION = "0.2.4"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
|
||||
|
||||
@@ -74,5 +74,188 @@ void main() {
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('create new calendar event using new button', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create the calendar view
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateCalendarButton();
|
||||
|
||||
// Scroll until today's date cell is visible
|
||||
await tester.scrollToToday();
|
||||
|
||||
// Hover over today's calendar cell
|
||||
await tester.hoverOnTodayCalendarCell();
|
||||
|
||||
// Tap on create new event button
|
||||
await tester.tapAddCalendarEventButton();
|
||||
|
||||
// Make sure that the row details page is opened
|
||||
tester.assertRowDetailPageOpened();
|
||||
|
||||
// Dismiss the row details page
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
// Make sure that the event is inserted in the cell
|
||||
tester.assertNumberOfEventsInCalendar(1);
|
||||
|
||||
// Create another event on the same day
|
||||
await tester.hoverOnTodayCalendarCell();
|
||||
await tester.tapAddCalendarEventButton();
|
||||
await tester.dismissRowDetailPage();
|
||||
tester.assertNumberOfEventsInCalendar(2);
|
||||
tester.assertNumberofEventsOnSpecificDay(2, DateTime.now());
|
||||
});
|
||||
|
||||
testWidgets('create new calendar event by double-clicking', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create the calendar view
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateCalendarButton();
|
||||
|
||||
// Scroll until today's date cell is visible
|
||||
await tester.scrollToToday();
|
||||
|
||||
// Double click on today's calendar cell to create a new event
|
||||
await tester.doubleClickCalendarCell(DateTime.now());
|
||||
|
||||
// Make sure that the row details page is opened
|
||||
tester.assertRowDetailPageOpened();
|
||||
|
||||
// Dismiss the row details page
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
// Make sure that the event is inserted in the cell
|
||||
tester.assertNumberOfEventsInCalendar(1);
|
||||
|
||||
// Create another event on the same day
|
||||
await tester.doubleClickCalendarCell(DateTime.now());
|
||||
await tester.dismissRowDetailPage();
|
||||
tester.assertNumberOfEventsInCalendar(2);
|
||||
tester.assertNumberofEventsOnSpecificDay(2, DateTime.now());
|
||||
});
|
||||
|
||||
testWidgets('duplicate/delete a calendar event', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create the calendar view
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateCalendarButton();
|
||||
|
||||
// Create a new event in today's calendar cell
|
||||
await tester.scrollToToday();
|
||||
await tester.doubleClickCalendarCell(DateTime.now());
|
||||
|
||||
// Duplicate the event
|
||||
await tester.tapRowDetailPageDuplicateRowButton();
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
// Check that there are 2 events
|
||||
tester.assertNumberOfEventsInCalendar(2);
|
||||
|
||||
// Delete an event
|
||||
await tester.openCalendarEvent(index: 0);
|
||||
await tester.tapRowDetailPageDeleteRowButton();
|
||||
|
||||
// Check that there is 1 event
|
||||
tester.assertNumberOfEventsInCalendar(1);
|
||||
});
|
||||
|
||||
testWidgets('edit the title of a calendar date event', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create the calendar view
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateCalendarButton();
|
||||
|
||||
// Create a new event in today's calendar cell
|
||||
await tester.scrollToToday();
|
||||
await tester.doubleClickCalendarCell(DateTime.now());
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
// Click on the event
|
||||
await tester.openCalendarEvent(index: 0);
|
||||
|
||||
// Make sure that the row details page is opened
|
||||
tester.assertRowDetailPageOpened();
|
||||
|
||||
// Change the title of the event
|
||||
await tester.editTitleInRowDetailPage('hello world');
|
||||
|
||||
// Dismiss the row details page
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
// Make sure that the event is edited
|
||||
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'edit the date of the date field being used to layout the calendar',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create the calendar view
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateCalendarButton();
|
||||
|
||||
// Create a new event in today's calendar cell
|
||||
await tester.scrollToToday();
|
||||
await tester.doubleClickCalendarCell(DateTime.now());
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
// Make sure that the event is today
|
||||
tester.assertNumberofEventsOnSpecificDay(1, DateTime.now());
|
||||
|
||||
// Click on the event
|
||||
await tester.openCalendarEvent(index: 0);
|
||||
|
||||
// Open the date editor of the event
|
||||
await tester.tapDateCellInRowDetailPage();
|
||||
await tester.findDateEditor(findsOneWidget);
|
||||
|
||||
// Edit the event's date
|
||||
final tomorrow = DateTime.now().add(const Duration(days: 1));
|
||||
await tester.selectDay(content: tomorrow.day);
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// Dismiss the row details page
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
// Make sure that the event is edited
|
||||
tester.assertNumberOfEventsInCalendar(1);
|
||||
tester.assertNumberofEventsOnSpecificDay(1, tomorrow);
|
||||
});
|
||||
|
||||
testWidgets('reschedule an event by drag-and-drop', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create the calendar view
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateCalendarButton();
|
||||
|
||||
// Create a new event on the first of this month
|
||||
final today = DateTime.now();
|
||||
final firstOfThisMonth = DateTime(today.year, today.month, 1);
|
||||
await tester.doubleClickCalendarCell(firstOfThisMonth);
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
// Drag and drop the event onto the next week, same day
|
||||
await tester.dragDropRescheduleCalendarEvent(firstOfThisMonth);
|
||||
|
||||
// Make sure that the event has been rescheduled to the new date
|
||||
tester.assertNumberOfEventsInCalendar(1);
|
||||
tester.assertNumberofEventsOnSpecificDay(
|
||||
1,
|
||||
firstOfThisMonth.add(const Duration(days: 7)),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
@@ -46,6 +48,44 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
// Makesure the text cells are filled with the right content when there are
|
||||
// multiple text cell
|
||||
testWidgets('edit multiple text cells', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
await tester.createNewPageWithName(ViewLayoutPB.Grid, 'my grid');
|
||||
await tester.createField(FieldType.RichText, 'description');
|
||||
|
||||
await tester.editCell(
|
||||
rowIndex: 0,
|
||||
fieldType: FieldType.RichText,
|
||||
input: 'hello',
|
||||
);
|
||||
|
||||
await tester.editCell(
|
||||
rowIndex: 0,
|
||||
fieldType: FieldType.RichText,
|
||||
input: 'world',
|
||||
cellIndex: 1,
|
||||
);
|
||||
|
||||
await tester.assertCellContent(
|
||||
rowIndex: 0,
|
||||
fieldType: FieldType.RichText,
|
||||
content: 'hello',
|
||||
cellIndex: 0,
|
||||
);
|
||||
|
||||
await tester.assertCellContent(
|
||||
rowIndex: 0,
|
||||
fieldType: FieldType.RichText,
|
||||
content: 'world',
|
||||
cellIndex: 1,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit number cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
@@ -176,8 +216,72 @@ void main() {
|
||||
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findDateEditor(findsOneWidget);
|
||||
|
||||
// Select the date
|
||||
await tester.selectDay(content: 3);
|
||||
// Toggle include time
|
||||
await tester.toggleIncludeTime();
|
||||
|
||||
// Dismiss the cell editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findDateEditor(findsOneWidget);
|
||||
|
||||
// Turn off include time
|
||||
await tester.toggleIncludeTime();
|
||||
|
||||
// Select a date
|
||||
final today = DateTime.now();
|
||||
await tester.selectDay(content: today.day);
|
||||
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
content: DateFormat('MMM d, y').format(today),
|
||||
);
|
||||
|
||||
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findDateEditor(findsOneWidget);
|
||||
|
||||
// Toggle include time
|
||||
final now = DateTime.now();
|
||||
await tester.toggleIncludeTime();
|
||||
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
content: DateFormat('MMM d, y HH:mm').format(now),
|
||||
);
|
||||
|
||||
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findDateEditor(findsOneWidget);
|
||||
|
||||
// Change date format
|
||||
await tester.changeDateFormat();
|
||||
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
content: DateFormat('dd/MM/y HH:mm').format(now),
|
||||
);
|
||||
|
||||
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findDateEditor(findsOneWidget);
|
||||
|
||||
// Change time format
|
||||
await tester.changeTimeFormat();
|
||||
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
content: DateFormat('dd/MM/y hh:mm a').format(now),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
@@ -195,13 +299,151 @@ void main() {
|
||||
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findSelectOptionEditor(findsOneWidget);
|
||||
|
||||
await tester.createOption(name: 'hello world');
|
||||
await tester.dismissSelectOptionEditor();
|
||||
// Create a new select option
|
||||
await tester.createOption(name: 'tag 1');
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// Make sure the option is created and displayed in the cell
|
||||
await tester.findSelectOptionWithNameInGrid(
|
||||
rowIndex: 0,
|
||||
name: 'hello world',
|
||||
name: 'tag 1',
|
||||
);
|
||||
|
||||
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findSelectOptionEditor(findsOneWidget);
|
||||
|
||||
// Create another select option
|
||||
await tester.createOption(name: 'tag 2');
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.findSelectOptionWithNameInGrid(
|
||||
rowIndex: 0,
|
||||
name: 'tag 2',
|
||||
);
|
||||
|
||||
await tester.assertNumberOfSelectedOptionsInGrid(
|
||||
rowIndex: 0,
|
||||
matcher: findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findSelectOptionEditor(findsOneWidget);
|
||||
|
||||
// switch to first option
|
||||
await tester.selectOption(name: 'tag 1');
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.findSelectOptionWithNameInGrid(
|
||||
rowIndex: 0,
|
||||
name: 'tag 1',
|
||||
);
|
||||
|
||||
await tester.assertNumberOfSelectedOptionsInGrid(
|
||||
rowIndex: 0,
|
||||
matcher: findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findSelectOptionEditor(findsOneWidget);
|
||||
|
||||
// Deselect the currently-selected option
|
||||
await tester.selectOption(name: 'tag 1');
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.assertNumberOfSelectedOptionsInGrid(
|
||||
rowIndex: 0,
|
||||
matcher: findsNothing,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit multi select cell', (tester) async {
|
||||
final tags = [
|
||||
'tag 1',
|
||||
'tag 2',
|
||||
'tag 3',
|
||||
'tag 4',
|
||||
];
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
const fieldType = FieldType.MultiSelect;
|
||||
await tester.createField(fieldType, fieldType.name);
|
||||
|
||||
// Tap the cell to invoke the selection option editor
|
||||
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findSelectOptionEditor(findsOneWidget);
|
||||
|
||||
// Create a new select option
|
||||
await tester.createOption(name: tags.first);
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// Make sure the option is created and displayed in the cell
|
||||
await tester.findSelectOptionWithNameInGrid(
|
||||
rowIndex: 0,
|
||||
name: tags.first,
|
||||
);
|
||||
|
||||
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findSelectOptionEditor(findsOneWidget);
|
||||
|
||||
// Create some other select options
|
||||
await tester.createOption(name: tags[1]);
|
||||
await tester.createOption(name: tags[2]);
|
||||
await tester.createOption(name: tags[3]);
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
for (final tag in tags) {
|
||||
await tester.findSelectOptionWithNameInGrid(
|
||||
rowIndex: 0,
|
||||
name: tag,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.assertNumberOfSelectedOptionsInGrid(
|
||||
rowIndex: 0,
|
||||
matcher: findsNWidgets(4),
|
||||
);
|
||||
|
||||
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findSelectOptionEditor(findsOneWidget);
|
||||
|
||||
// Deselect all options
|
||||
for (final tag in tags) {
|
||||
await tester.selectOption(name: tag);
|
||||
}
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.assertNumberOfSelectedOptionsInGrid(
|
||||
rowIndex: 0,
|
||||
matcher: findsNothing,
|
||||
);
|
||||
|
||||
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||
await tester.findSelectOptionEditor(findsOneWidget);
|
||||
|
||||
// Select some options
|
||||
await tester.selectOption(name: tags[1]);
|
||||
await tester.selectOption(name: tags[3]);
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.findSelectOptionWithNameInGrid(
|
||||
rowIndex: 0,
|
||||
name: tags[1],
|
||||
);
|
||||
await tester.findSelectOptionWithNameInGrid(
|
||||
rowIndex: 0,
|
||||
name: tags[3],
|
||||
);
|
||||
|
||||
await tester.assertNumberOfSelectedOptionsInGrid(
|
||||
rowIndex: 0,
|
||||
matcher: findsNWidgets(2),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -28,6 +28,21 @@ void main() {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets('row details page opens', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create a new grid
|
||||
await tester.tapAddButton();
|
||||
await tester.tapCreateGridButton();
|
||||
|
||||
// Hover first row and then open the row page
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
// Make sure that the row page is opened
|
||||
tester.assertRowDetailPageOpened();
|
||||
});
|
||||
|
||||
testWidgets('insert emoji in the row detail page', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -127,6 +127,29 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> doubleTapButton(
|
||||
Finder finder, {
|
||||
int? pointer,
|
||||
int buttons = kPrimaryButton,
|
||||
bool warnIfMissed = true,
|
||||
int milliseconds = 500,
|
||||
}) async {
|
||||
await tapButton(
|
||||
finder,
|
||||
pointer: pointer,
|
||||
buttons: buttons,
|
||||
warnIfMissed: warnIfMissed,
|
||||
milliseconds: kDoubleTapMinTime.inMilliseconds,
|
||||
);
|
||||
await tapButton(
|
||||
finder,
|
||||
pointer: pointer,
|
||||
buttons: buttons,
|
||||
warnIfMissed: warnIfMissed,
|
||||
milliseconds: milliseconds,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> wait(int milliseconds) async {
|
||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
||||
return;
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_day.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart';
|
||||
@@ -20,6 +21,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/cr
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_menu.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart';
|
||||
@@ -35,6 +37,7 @@ import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/database_setting.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -106,22 +109,26 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
required int rowIndex,
|
||||
required FieldType fieldType,
|
||||
required String input,
|
||||
int cellIndex = 0,
|
||||
}) async {
|
||||
final cell = cellFinder(rowIndex, fieldType);
|
||||
final cell = cellFinder(rowIndex, fieldType, cellIndex: cellIndex);
|
||||
|
||||
expect(cell, findsOneWidget);
|
||||
await enterText(cell, input);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Finder cellFinder(int rowIndex, FieldType fieldType) {
|
||||
///
|
||||
Finder cellFinder(int rowIndex, FieldType fieldType, {int cellIndex = 0}) {
|
||||
final findRow = find.byType(GridRow, skipOffstage: false);
|
||||
final findCell = finderForFieldType(fieldType);
|
||||
return find.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: findCell,
|
||||
skipOffstage: false,
|
||||
);
|
||||
return find
|
||||
.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: findCell,
|
||||
skipOffstage: false,
|
||||
)
|
||||
.at(cellIndex);
|
||||
}
|
||||
|
||||
Future<void> tapCheckboxCellInGrid({
|
||||
@@ -166,13 +173,14 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(cell, warnIfMissed: false);
|
||||
}
|
||||
|
||||
/// The [fieldName] must be uqniue in the grid.
|
||||
/// The [fieldName] must be unique in the grid.
|
||||
Future<void> assertCellContent({
|
||||
required int rowIndex,
|
||||
required FieldType fieldType,
|
||||
required String content,
|
||||
int cellIndex = 0,
|
||||
}) async {
|
||||
final findCell = cellFinder(rowIndex, fieldType);
|
||||
final findCell = cellFinder(rowIndex, fieldType, cellIndex: cellIndex);
|
||||
final findContent = find.descendant(
|
||||
of: findCell,
|
||||
matching: find.text(content),
|
||||
@@ -289,6 +297,48 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(finder);
|
||||
}
|
||||
|
||||
Future<void> toggleIncludeTime() async {
|
||||
final findDateEditor = find.byType(DateCellEditor);
|
||||
final findToggle = find.byType(Toggle);
|
||||
final finder = find.descendant(
|
||||
of: findDateEditor,
|
||||
matching: findToggle,
|
||||
);
|
||||
await tapButton(finder);
|
||||
}
|
||||
|
||||
Future<void> changeDateFormat() async {
|
||||
final findDateEditor = find.byType(DateCellEditor);
|
||||
final findDateTimeOptionButton = find.byType(DateTypeOptionButton);
|
||||
final finder = find.descendant(
|
||||
of: findDateEditor,
|
||||
matching: findDateTimeOptionButton,
|
||||
);
|
||||
await tapButton(finder);
|
||||
|
||||
final findDateFormatButton = find.byType(DateFormatButton);
|
||||
await tapButton(findDateFormatButton);
|
||||
|
||||
final findNewDateFormat = find.text("Day/Month/Year");
|
||||
await tapButton(findNewDateFormat);
|
||||
}
|
||||
|
||||
Future<void> changeTimeFormat() async {
|
||||
final findDateEditor = find.byType(DateCellEditor);
|
||||
final findDateTimeOptionButton = find.byType(DateTypeOptionButton);
|
||||
final finder = find.descendant(
|
||||
of: findDateEditor,
|
||||
matching: findDateTimeOptionButton,
|
||||
);
|
||||
await tapButton(finder);
|
||||
|
||||
final findDateFormatButton = find.byType(TimeFormatButton);
|
||||
await tapButton(findDateFormatButton);
|
||||
|
||||
final findNewDateFormat = find.text("12 hour");
|
||||
await tapButton(findNewDateFormat);
|
||||
}
|
||||
|
||||
Future<void> tapSelectOptionCellInGrid({
|
||||
required int rowIndex,
|
||||
required FieldType fieldType,
|
||||
@@ -325,6 +375,16 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> selectOption({
|
||||
required String name,
|
||||
}) async {
|
||||
final option = find.byWidgetPredicate(
|
||||
(widget) => widget is SelectOptionTagCell && widget.option.name == name,
|
||||
);
|
||||
|
||||
await tapButton(option);
|
||||
}
|
||||
|
||||
Future<void> findSelectOptionWithNameInGrid({
|
||||
required int rowIndex,
|
||||
required String name,
|
||||
@@ -342,6 +402,24 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
expect(cell, findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> assertNumberOfSelectedOptionsInGrid({
|
||||
required int rowIndex,
|
||||
required Matcher matcher,
|
||||
}) async {
|
||||
final findRow = find.byType(GridRow);
|
||||
|
||||
final options = find.byWidgetPredicate(
|
||||
(widget) => widget is SelectOptionTag,
|
||||
);
|
||||
|
||||
final cell = find.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: options,
|
||||
);
|
||||
|
||||
expect(cell, matcher);
|
||||
}
|
||||
|
||||
Future<void> openFirstRowDetailPage() async {
|
||||
await hoverOnFirstRowOfGrid();
|
||||
|
||||
@@ -350,6 +428,22 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(expandButton);
|
||||
}
|
||||
|
||||
void assertRowDetailPageOpened() async {
|
||||
final findRowDetailPage = find.byType(RowDetailPage);
|
||||
expect(findRowDetailPage, findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> dismissRowDetailPage() async {
|
||||
await sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> editTitleInRowDetailPage(String title) async {
|
||||
final titleField = find.byType(GridTextCell);
|
||||
await enterText(titleField, title);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> hoverRowBanner() async {
|
||||
final banner = find.byType(RowBanner);
|
||||
expect(banner, findsOneWidget);
|
||||
@@ -378,6 +472,21 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(emojiWidget);
|
||||
}
|
||||
|
||||
Future<void> tapDateCellInRowDetailPage() async {
|
||||
final findDateCell = find.byType(GridDateCell);
|
||||
await tapButton(findDateCell);
|
||||
}
|
||||
|
||||
Future<void> duplicateRowInRowDetailPage() async {
|
||||
final duplicateButton = find.byType(RowDetailPageDuplicateButton);
|
||||
await tapButton(duplicateButton);
|
||||
}
|
||||
|
||||
Future<void> deleteRowInRowDetailPage() async {
|
||||
final deleteButton = find.byType(RowDetailPageDeleteButton);
|
||||
await tapButton(deleteButton);
|
||||
}
|
||||
|
||||
Future<void> scrollGridByOffset(Offset offset) async {
|
||||
await drag(find.byType(GridPage), offset);
|
||||
await pumpAndSettle();
|
||||
@@ -560,7 +669,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
expect(finder, matcher);
|
||||
}
|
||||
|
||||
Future<void> dismissSelectOptionEditor() async {
|
||||
Future<void> dismissCellEditor() async {
|
||||
await sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
@@ -866,6 +975,94 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
expect(finder, findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> scrollToToday() async {
|
||||
final todayCell = find.byWidgetPredicate(
|
||||
(widget) => widget is CalendarDayCard && widget.isToday,
|
||||
skipOffstage: false,
|
||||
);
|
||||
await ensureVisible(todayCell);
|
||||
await pumpAndSettle(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
Future<void> hoverOnTodayCalendarCell() async {
|
||||
final todayCell = find.byWidgetPredicate(
|
||||
(widget) => widget is CalendarDayCard && widget.isToday,
|
||||
);
|
||||
|
||||
await hoverOnWidget(todayCell);
|
||||
}
|
||||
|
||||
Future<void> tapAddCalendarEventButton() async {
|
||||
final findFlowyButton = find.byType(FlowyIconButton);
|
||||
final findNewEventButton = find.byType(NewEventButton);
|
||||
final button = find.descendant(
|
||||
of: findNewEventButton,
|
||||
matching: findFlowyButton,
|
||||
);
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
/// Checks for a certain number of events. Parameters [date] and [title] can
|
||||
/// also be provided to restrict the scope of the search
|
||||
void assertNumberOfEventsInCalendar(int number, {String? title}) {
|
||||
Finder findEvents = find.byType(EventCard);
|
||||
if (title != null) {
|
||||
findEvents = find.descendant(of: findEvents, matching: find.text(title));
|
||||
}
|
||||
expect(findEvents, findsNWidgets(number));
|
||||
}
|
||||
|
||||
void assertNumberofEventsOnSpecificDay(
|
||||
int number,
|
||||
DateTime date, {
|
||||
String? title,
|
||||
}) {
|
||||
final findDayCell = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is CalendarDayCard &&
|
||||
isSameDay(
|
||||
widget.date,
|
||||
date,
|
||||
),
|
||||
);
|
||||
Finder findEvents = find.descendant(
|
||||
of: findDayCell,
|
||||
matching: find.byType(EventCard),
|
||||
);
|
||||
if (title != null) {
|
||||
findEvents = find.descendant(of: findEvents, matching: find.text(title));
|
||||
}
|
||||
expect(findEvents, findsNWidgets(number));
|
||||
}
|
||||
|
||||
Future<void> doubleClickCalendarCell(DateTime date) async {
|
||||
final todayCell = find.byWidgetPredicate(
|
||||
(widget) => widget is CalendarDayCard && isSameDay(date, widget.date),
|
||||
);
|
||||
|
||||
await doubleTapButton(todayCell);
|
||||
}
|
||||
|
||||
Future<void> openCalendarEvent({required index, DateTime? date}) async {
|
||||
final findDayCell = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is CalendarDayCard &&
|
||||
isSameDay(widget.date, date ?? DateTime.now()),
|
||||
);
|
||||
final cards = find.descendant(
|
||||
of: findDayCell,
|
||||
matching: find.byType(EventCard),
|
||||
);
|
||||
|
||||
await tapButton(cards.at(index));
|
||||
}
|
||||
|
||||
Future<void> dragDropRescheduleCalendarEvent(DateTime startDate) async {
|
||||
final findEventCard = find.byType(EventCard);
|
||||
await drag(findEventCard.first, const Offset(0, 300));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> tapCreateLinkedDatabaseViewButton(AddButtonAction action) async {
|
||||
final findAddButton = find.byType(AddDatabaseViewButton);
|
||||
await tapButton(findAddButton);
|
||||
|
||||
+8
-6
@@ -113,7 +113,7 @@ class CalendarDayCard extends StatelessWidget {
|
||||
.add(CalendarEvent.moveEvent(event, date));
|
||||
},
|
||||
),
|
||||
_NewEventButton(onCreate: () => onCreateEvent(date)),
|
||||
NewEventButton(onCreate: () => onCreateEvent(date)),
|
||||
MouseRegion(
|
||||
onEnter: (p) => notifyEnter(context, true),
|
||||
onExit: (p) => notifyEnter(context, false),
|
||||
@@ -160,9 +160,10 @@ class _Header extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _NewEventButton extends StatelessWidget {
|
||||
@visibleForTesting
|
||||
class NewEventButton extends StatelessWidget {
|
||||
final VoidCallback onCreate;
|
||||
const _NewEventButton({required this.onCreate, Key? key}) : super(key: key);
|
||||
const NewEventButton({required this.onCreate, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -254,7 +255,7 @@ class _EventList extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Flexible(
|
||||
child: ListView.separated(
|
||||
itemBuilder: (BuildContext context, int index) => _EventCard(
|
||||
itemBuilder: (BuildContext context, int index) => EventCard(
|
||||
event: events[index],
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
@@ -270,13 +271,14 @@ class _EventList extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _EventCard extends StatelessWidget {
|
||||
@visibleForTesting
|
||||
class EventCard extends StatelessWidget {
|
||||
final CalendarDayEvent event;
|
||||
final String viewId;
|
||||
final RowCache rowCache;
|
||||
final BoxConstraints constraints;
|
||||
|
||||
const _EventCard({
|
||||
const EventCard({
|
||||
required this.event,
|
||||
required this.viewId,
|
||||
required this.rowCache,
|
||||
|
||||
+63
-35
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
|
||||
@@ -241,41 +243,7 @@ class _GridRows extends StatelessWidget {
|
||||
);
|
||||
return ScrollConfiguration(
|
||||
behavior: behavior,
|
||||
child: ReorderableListView.builder(
|
||||
/// TODO(Xazin): Resolve inconsistent scrollbar behavior
|
||||
/// This is a workaround related to
|
||||
/// https://github.com/flutter/flutter/issues/25652
|
||||
cacheExtent: 5000,
|
||||
scrollController: scrollController.verticalController,
|
||||
buildDefaultDragHandles: false,
|
||||
proxyDecorator: (child, index, animation) => Material(
|
||||
color: Colors.white.withOpacity(.1),
|
||||
child: Opacity(opacity: .5, child: child),
|
||||
),
|
||||
onReorder: (fromIndex, newIndex) {
|
||||
final toIndex =
|
||||
newIndex > fromIndex ? newIndex - 1 : newIndex;
|
||||
if (fromIndex == toIndex) {
|
||||
return;
|
||||
}
|
||||
context
|
||||
.read<GridBloc>()
|
||||
.add(GridEvent.moveRow(fromIndex, toIndex));
|
||||
},
|
||||
itemCount: rowInfos.length + 1, // the extra item is the footer
|
||||
itemBuilder: (context, index) {
|
||||
if (index < rowInfos.length) {
|
||||
final rowInfo = rowInfos[index];
|
||||
return _renderRow(
|
||||
context,
|
||||
rowInfo.rowId,
|
||||
isDraggable: state.reorderable,
|
||||
index: index,
|
||||
);
|
||||
}
|
||||
return const GridRowBottomBar(key: Key('gridFooter'));
|
||||
},
|
||||
),
|
||||
child: _renderList(context, state, rowInfos),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -283,6 +251,66 @@ class _GridRows extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _renderList(
|
||||
BuildContext context,
|
||||
GridState state,
|
||||
List<RowInfo> rowInfos,
|
||||
) {
|
||||
if (Platform.isWindows) {
|
||||
// Workaround: On Windows, the focusing of the text cell is not working
|
||||
// properly when the list is reorderable. So using the ListView instead.
|
||||
return ListView.builder(
|
||||
controller: scrollController.verticalController,
|
||||
itemCount: rowInfos.length + 1, // the extra item is the footer
|
||||
itemBuilder: (context, index) {
|
||||
if (index < rowInfos.length) {
|
||||
final rowInfo = rowInfos[index];
|
||||
return _renderRow(
|
||||
context,
|
||||
rowInfo.rowId,
|
||||
isDraggable: false,
|
||||
index: index,
|
||||
);
|
||||
}
|
||||
return const GridRowBottomBar(key: Key('gridFooter'));
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return ReorderableListView.builder(
|
||||
/// TODO(Xazin): Resolve inconsistent scrollbar behavior
|
||||
/// This is a workaround related to
|
||||
/// https://github.com/flutter/flutter/issues/25652
|
||||
cacheExtent: 5000,
|
||||
scrollController: scrollController.verticalController,
|
||||
buildDefaultDragHandles: false,
|
||||
proxyDecorator: (child, index, animation) => Material(
|
||||
color: Colors.white.withOpacity(.1),
|
||||
child: Opacity(opacity: .5, child: child),
|
||||
),
|
||||
onReorder: (fromIndex, newIndex) {
|
||||
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
|
||||
if (fromIndex == toIndex) {
|
||||
return;
|
||||
}
|
||||
context.read<GridBloc>().add(GridEvent.moveRow(fromIndex, toIndex));
|
||||
},
|
||||
itemCount: rowInfos.length + 1, // the extra item is the footer
|
||||
itemBuilder: (context, index) {
|
||||
if (index < rowInfos.length) {
|
||||
final rowInfo = rowInfos[index];
|
||||
return _renderRow(
|
||||
context,
|
||||
rowInfo.rowId,
|
||||
isDraggable: state.reorderable,
|
||||
index: index,
|
||||
);
|
||||
}
|
||||
return const GridRowBottomBar(key: Key('gridFooter'));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _renderRow(
|
||||
BuildContext context,
|
||||
RowId rowId, {
|
||||
|
||||
-1
@@ -138,7 +138,6 @@ class _OptionNameTextField extends StatelessWidget {
|
||||
return FlowyTextField(
|
||||
autoFocus: autoFocus,
|
||||
text: name,
|
||||
maxLength: 30,
|
||||
submitOnLeave: true,
|
||||
onSubmitted: (newName) {
|
||||
if (name != newName) {
|
||||
|
||||
@@ -103,11 +103,11 @@ class BlankCell extends StatelessWidget {
|
||||
}
|
||||
|
||||
abstract class CellEditable {
|
||||
GridCellFocusListener get beginFocus;
|
||||
RequestFocusListener get requestFocus;
|
||||
|
||||
ValueNotifier<bool> get onCellFocus;
|
||||
|
||||
ValueNotifier<bool> get onCellEditing;
|
||||
// ValueNotifier<bool> get onCellEditing;
|
||||
}
|
||||
|
||||
typedef AccessoryBuilder = List<GridCellAccessoryBuilder> Function(
|
||||
@@ -125,11 +125,7 @@ abstract class CellAccessory extends Widget {
|
||||
|
||||
abstract class GridCellWidget extends StatefulWidget
|
||||
implements CellAccessory, CellEditable, CellShortcuts {
|
||||
GridCellWidget({Key? key}) : super(key: key) {
|
||||
onCellEditing.addListener(() {
|
||||
onCellFocus.value = onCellEditing.value;
|
||||
});
|
||||
}
|
||||
GridCellWidget({super.key});
|
||||
|
||||
@override
|
||||
final ValueNotifier<bool> onCellFocus = ValueNotifier<bool>(false);
|
||||
@@ -138,8 +134,8 @@ abstract class GridCellWidget extends StatefulWidget
|
||||
@override
|
||||
ValueNotifier<bool> get onAccessoryHover => onCellFocus;
|
||||
|
||||
@override
|
||||
final ValueNotifier<bool> onCellEditing = ValueNotifier<bool>(false);
|
||||
// @override
|
||||
// final ValueNotifier<bool> onCellEditing = ValueNotifier<bool>(false);
|
||||
|
||||
@override
|
||||
List<GridCellAccessoryBuilder> Function(
|
||||
@@ -147,7 +143,7 @@ abstract class GridCellWidget extends StatefulWidget
|
||||
)? get accessoryBuilder => null;
|
||||
|
||||
@override
|
||||
final GridCellFocusListener beginFocus = GridCellFocusListener();
|
||||
final RequestFocusListener requestFocus = RequestFocusListener();
|
||||
|
||||
@override
|
||||
final Map<CellKeyboardKey, CellKeyboardAction> shortcutHandlers = {};
|
||||
@@ -156,7 +152,7 @@ abstract class GridCellWidget extends StatefulWidget
|
||||
abstract class GridCellState<T extends GridCellWidget> extends State<T> {
|
||||
@override
|
||||
void initState() {
|
||||
widget.beginFocus.setListener(() => requestBeginFocus());
|
||||
widget.requestFocus.setListener(requestBeginFocus);
|
||||
widget.shortcutHandlers[CellKeyboardKey.onCopy] = () => onCopy();
|
||||
widget.shortcutHandlers[CellKeyboardKey.onInsert] = () {
|
||||
Clipboard.getData("text/plain").then((data) {
|
||||
@@ -172,17 +168,18 @@ abstract class GridCellState<T extends GridCellWidget> extends State<T> {
|
||||
@override
|
||||
void didUpdateWidget(covariant T oldWidget) {
|
||||
if (oldWidget != this) {
|
||||
widget.beginFocus.setListener(() => requestBeginFocus());
|
||||
widget.requestFocus.setListener(requestBeginFocus);
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.beginFocus.removeAllListener();
|
||||
widget.requestFocus.removeAllListener();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Subclass can override this method to request focus.
|
||||
void requestBeginFocus();
|
||||
|
||||
String? onCopy() => null;
|
||||
@@ -190,9 +187,9 @@ abstract class GridCellState<T extends GridCellWidget> extends State<T> {
|
||||
void onInsert(String value) {}
|
||||
}
|
||||
|
||||
abstract class GridFocusNodeCellState<T extends GridCellWidget>
|
||||
abstract class GridEditableTextCell<T extends GridCellWidget>
|
||||
extends GridCellState<T> {
|
||||
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
|
||||
SingleListenerFocusNode get focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -226,9 +223,9 @@ abstract class GridFocusNodeCellState<T extends GridCellWidget>
|
||||
}
|
||||
|
||||
void _listenOnFocusNodeChanged() {
|
||||
widget.onCellEditing.value = focusNode.hasFocus;
|
||||
widget.onCellFocus.value = focusNode.hasFocus;
|
||||
focusNode.setListener(() {
|
||||
widget.onCellEditing.value = focusNode.hasFocus;
|
||||
widget.onCellFocus.value = focusNode.hasFocus;
|
||||
focusChanged();
|
||||
});
|
||||
}
|
||||
@@ -236,7 +233,7 @@ abstract class GridFocusNodeCellState<T extends GridCellWidget>
|
||||
Future<void> focusChanged() async {}
|
||||
}
|
||||
|
||||
class GridCellFocusListener extends ChangeNotifier {
|
||||
class RequestFocusListener extends ChangeNotifier {
|
||||
VoidCallback? _listener;
|
||||
|
||||
void setListener(VoidCallback listener) {
|
||||
|
||||
+2
-2
@@ -51,8 +51,8 @@ class CellContainer extends StatelessWidget {
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => child.beginFocus.notify(),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => child.requestFocus.notify(),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: width, minHeight: 46),
|
||||
decoration: _makeBoxDecoration(context, isFocus),
|
||||
|
||||
+2
-2
@@ -45,14 +45,14 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
popupBuilder: (BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.onCellEditing.value = true;
|
||||
widget.onCellFocus.value = true;
|
||||
});
|
||||
return GridChecklistCellEditor(
|
||||
cellController:
|
||||
widget.cellControllerBuilder.build() as ChecklistCellController,
|
||||
);
|
||||
},
|
||||
onClose: () => widget.onCellEditing.value = false,
|
||||
onClose: () => widget.onCellFocus.value = false,
|
||||
child: Padding(
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
|
||||
|
||||
+3
-3
@@ -90,11 +90,11 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
||||
return DateCellEditor(
|
||||
cellController: widget.cellControllerBuilder.build()
|
||||
as DateCellController,
|
||||
onDismissed: () => widget.onCellEditing.value = false,
|
||||
onDismissed: () => widget.onCellFocus.value = false,
|
||||
);
|
||||
},
|
||||
onClose: () {
|
||||
widget.onCellEditing.value = false;
|
||||
widget.onCellFocus.value = false;
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -115,7 +115,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
||||
_popover.show();
|
||||
|
||||
if (widget.editable) {
|
||||
widget.onCellEditing.value = true;
|
||||
widget.onCellFocus.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -123,7 +123,7 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
|
||||
const TypeOptionSeparator(spacing: 12.0),
|
||||
const _IncludeTimeButton(),
|
||||
const TypeOptionSeparator(spacing: 12.0),
|
||||
_DateTypeOptionButton(popoverMutex: popoverMutex)
|
||||
DateTypeOptionButton(popoverMutex: popoverMutex)
|
||||
];
|
||||
|
||||
return ListView.builder(
|
||||
@@ -337,9 +337,10 @@ class _TimeTextFieldState extends State<_TimeTextField> {
|
||||
}
|
||||
}
|
||||
|
||||
class _DateTypeOptionButton extends StatelessWidget {
|
||||
@visibleForTesting
|
||||
class DateTypeOptionButton extends StatelessWidget {
|
||||
final PopoverMutex popoverMutex;
|
||||
const _DateTypeOptionButton({
|
||||
const DateTypeOptionButton({
|
||||
required this.popoverMutex,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
+5
-2
@@ -16,13 +16,16 @@ class GridNumberCell extends GridCellWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
GridFocusNodeCellState<GridNumberCell> createState() => _NumberCellState();
|
||||
GridEditableTextCell<GridNumberCell> createState() => _NumberCellState();
|
||||
}
|
||||
|
||||
class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
|
||||
class _NumberCellState extends GridEditableTextCell<GridNumberCell> {
|
||||
late NumberCellBloc _cellBloc;
|
||||
late TextEditingController _controller;
|
||||
|
||||
@override
|
||||
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellController =
|
||||
|
||||
+2
-2
@@ -62,7 +62,7 @@ class _SingleSelectCellState extends GridCellState<GridSingleSelectCell> {
|
||||
return SelectOptionWrap(
|
||||
selectOptions: state.selectedOptions,
|
||||
cellStyle: widget.cellStyle,
|
||||
onCellEditing: widget.onCellEditing,
|
||||
onCellEditing: widget.onCellFocus,
|
||||
popoverController: _popover,
|
||||
cellControllerBuilder: widget.cellControllerBuilder,
|
||||
);
|
||||
@@ -125,7 +125,7 @@ class _MultiSelectCellState extends GridCellState<GridMultiSelectCell> {
|
||||
return SelectOptionWrap(
|
||||
selectOptions: state.selectedOptions,
|
||||
cellStyle: widget.cellStyle,
|
||||
onCellEditing: widget.onCellEditing,
|
||||
onCellEditing: widget.onCellFocus,
|
||||
popoverController: _popover,
|
||||
cellControllerBuilder: widget.cellControllerBuilder,
|
||||
);
|
||||
|
||||
-1
@@ -157,7 +157,6 @@ class _TextField extends StatelessWidget {
|
||||
options: state.options,
|
||||
selectedOptionMap: optionMap,
|
||||
distanceToText: _editorPanelWidth * 0.7,
|
||||
maxLength: 30,
|
||||
tagController: tagController,
|
||||
textSeparators: const [','],
|
||||
onClick: () => popoverMutex.close(),
|
||||
|
||||
+5
-2
@@ -41,13 +41,16 @@ class GridTextCell extends GridCellWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
GridFocusNodeCellState<GridTextCell> createState() => _GridTextCellState();
|
||||
GridEditableTextCell<GridTextCell> createState() => _GridTextCellState();
|
||||
}
|
||||
|
||||
class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
|
||||
class _GridTextCellState extends GridEditableTextCell<GridTextCell> {
|
||||
late TextCellBloc _cellBloc;
|
||||
late TextEditingController _controller;
|
||||
|
||||
@override
|
||||
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellController =
|
||||
|
||||
+5
-2
@@ -108,11 +108,14 @@ class GridURLCell extends GridCellWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _GridURLCellState extends GridFocusNodeCellState<GridURLCell> {
|
||||
class _GridURLCellState extends GridEditableTextCell<GridURLCell> {
|
||||
final _popoverController = PopoverController();
|
||||
late final URLCellBloc _cellBloc;
|
||||
late final TextEditingController _controller;
|
||||
|
||||
@override
|
||||
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -182,7 +185,7 @@ class _GridURLCellState extends GridFocusNodeCellState<GridURLCell> {
|
||||
|
||||
@override
|
||||
void requestBeginFocus() {
|
||||
widget.onCellEditing.value = true;
|
||||
widget.onCellFocus.value = true;
|
||||
_popoverController.show();
|
||||
}
|
||||
|
||||
|
||||
@@ -85,8 +85,8 @@ class _PropertyCellState extends State<_PropertyCell> {
|
||||
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
|
||||
|
||||
final gesture = GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => cell.beginFocus.notify(),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => cell.requestFocus.notify(),
|
||||
child: AccessoryHover(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 3, vertical: 3),
|
||||
child: cell,
|
||||
@@ -97,8 +97,8 @@ class _PropertyCellState extends State<_PropertyCell> {
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 30),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
AppFlowyPopover(
|
||||
controller: popover,
|
||||
@@ -108,6 +108,7 @@ class _PropertyCellState extends State<_PropertyCell> {
|
||||
popupBuilder: (popoverContext) => buildFieldEditor(),
|
||||
child: SizedBox(
|
||||
width: 150,
|
||||
height: 40,
|
||||
child: FieldCellButton(
|
||||
field: widget.cellContext.fieldInfo.field,
|
||||
onTap: () => popover.show(),
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/database/r
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@@ -132,6 +133,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
characterShortcutEvents: characterShortcutEvents,
|
||||
commandShortcutEvents: commandShortcutEvents,
|
||||
header: widget.header,
|
||||
footer: const VSpace(200),
|
||||
);
|
||||
|
||||
return Center(
|
||||
@@ -161,7 +163,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
];
|
||||
|
||||
final configuration = BlockComponentConfiguration(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 4.0),
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 5.0),
|
||||
);
|
||||
final customBlockComponentBuilderMap = {
|
||||
PageBlockKeys.type: PageBlockComponentBuilder(),
|
||||
@@ -211,7 +213,10 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
CalloutBlockKeys.type: CalloutBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
),
|
||||
DividerBlockKeys.type: DividerBlockComponentBuilder(),
|
||||
DividerBlockKeys.type: DividerBlockComponentBuilder(
|
||||
configuration: configuration,
|
||||
height: 28.0,
|
||||
),
|
||||
MathEquationBlockKeys.type: MathEquationBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(
|
||||
padding: (_) => const EdgeInsets.symmetric(vertical: 20),
|
||||
@@ -277,7 +282,13 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
];
|
||||
|
||||
builder.showActions = (_) => true;
|
||||
builder.actionBuilder = (context, state) => BlockActionList(
|
||||
builder.actionBuilder = (context, state) {
|
||||
final padding = context.node.type == HeadingBlockKeys.type
|
||||
? const EdgeInsets.only(top: 8.0)
|
||||
: const EdgeInsets.all(0);
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: BlockActionList(
|
||||
blockComponentContext: context,
|
||||
blockComponentState: state,
|
||||
editorState: widget.editorState,
|
||||
@@ -285,7 +296,9 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
showSlashMenu: () => showSlashMenu(
|
||||
widget.editorState,
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
return builders;
|
||||
|
||||
+2
-2
@@ -31,14 +31,14 @@ class BlockActionList extends StatelessWidget {
|
||||
editorState: editorState,
|
||||
showSlashMenu: showSlashMenu,
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
const SizedBox(width: 4.0),
|
||||
BlockOptionButton(
|
||||
blockComponentContext: blockComponentContext,
|
||||
blockComponentState: blockComponentState,
|
||||
actions: actions,
|
||||
editorState: editorState,
|
||||
),
|
||||
const SizedBox(width: 6.0),
|
||||
const SizedBox(width: 4.0),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
+5
-1
@@ -150,7 +150,11 @@ class _CalloutBlockComponentWidgetState
|
||||
children: [
|
||||
// the emoji picker button for the note
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
padding: const EdgeInsets.only(
|
||||
top: 6.0,
|
||||
left: 2.0,
|
||||
right: 2.0,
|
||||
),
|
||||
child: EmojiPickerButton(
|
||||
key: ValueKey(
|
||||
emoji.toString(),
|
||||
|
||||
+4
-4
@@ -39,7 +39,7 @@ class _EmojiPopoverState extends State<EmojiPopover> {
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: DeleteButton(onPressed: widget.removeIcon),
|
||||
child: DeleteButton(onTap: widget.removeIcon),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -67,13 +67,13 @@ class _EmojiPopoverState extends State<EmojiPopover> {
|
||||
}
|
||||
|
||||
class DeleteButton extends StatelessWidget {
|
||||
final VoidCallback onPressed;
|
||||
const DeleteButton({required this.onPressed, Key? key}) : super(key: key);
|
||||
final VoidCallback onTap;
|
||||
const DeleteButton({required this.onTap, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyButton(
|
||||
onTap: () => onPressed,
|
||||
onTap: onTap,
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText(
|
||||
LocaleKeys.document_plugins_cover_removeIcon.tr(),
|
||||
|
||||
@@ -118,7 +118,8 @@ class ShareActionListState extends State<ShareActionList> {
|
||||
case ShareAction.markdown:
|
||||
final exportPath = await getIt<FilePickerService>().saveFile(
|
||||
dialogTitle: '',
|
||||
fileName: '$name.md',
|
||||
// encode the file name in case it contains special characters
|
||||
fileName: '${Uri.encodeComponent(name)}.md',
|
||||
);
|
||||
if (exportPath != null) {
|
||||
docShareBloc.add(DocShareEvent.shareMarkdown(exportPath));
|
||||
|
||||
@@ -87,6 +87,7 @@ class MenuAppHeader extends StatelessWidget {
|
||||
case AppDisclosureAction.rename:
|
||||
NavigatorTextFieldDialog(
|
||||
title: LocaleKeys.menuAppHeader_renameDialog.tr(),
|
||||
autoSelectAllText: true,
|
||||
value: context.read<AppBloc>().state.view.name,
|
||||
confirm: (newValue) {
|
||||
context.read<AppBloc>().add(AppEvent.rename(newValue));
|
||||
|
||||
@@ -99,6 +99,7 @@ class ViewSectionItem extends StatelessWidget {
|
||||
case ViewDisclosureAction.rename:
|
||||
NavigatorTextFieldDialog(
|
||||
title: LocaleKeys.disclosureAction_rename.tr(),
|
||||
autoSelectAllText: true,
|
||||
value: blocContext.read<ViewBloc>().state.view.name,
|
||||
confirm: (newValue) {
|
||||
blocContext
|
||||
|
||||
@@ -12,29 +12,40 @@ export 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
|
||||
class NavigatorTextFieldDialog extends StatefulWidget {
|
||||
const NavigatorTextFieldDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.autoSelectAllText = false,
|
||||
required this.value,
|
||||
required this.confirm,
|
||||
this.cancel,
|
||||
});
|
||||
|
||||
final String value;
|
||||
final String title;
|
||||
final void Function()? cancel;
|
||||
final void Function(String) confirm;
|
||||
|
||||
const NavigatorTextFieldDialog({
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.confirm,
|
||||
this.cancel,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
final bool autoSelectAllText;
|
||||
|
||||
@override
|
||||
State<NavigatorTextFieldDialog> createState() => _CreateTextFieldDialog();
|
||||
State<NavigatorTextFieldDialog> createState() =>
|
||||
_NavigatorTextFieldDialogState();
|
||||
}
|
||||
|
||||
class _CreateTextFieldDialog extends State<NavigatorTextFieldDialog> {
|
||||
class _NavigatorTextFieldDialogState extends State<NavigatorTextFieldDialog> {
|
||||
String newValue = "";
|
||||
final controller = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
newValue = widget.value;
|
||||
controller.text = newValue;
|
||||
if (widget.autoSelectAllText) {
|
||||
controller.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: newValue.length,
|
||||
);
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -52,7 +63,7 @@ class _CreateTextFieldDialog extends State<NavigatorTextFieldDialog> {
|
||||
FlowyFormTextInput(
|
||||
textAlign: TextAlign.center,
|
||||
hintText: LocaleKeys.dialogCreatePageNameHint.tr(),
|
||||
initialValue: widget.value,
|
||||
controller: controller,
|
||||
textStyle: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontSize: FontSizes.s16,
|
||||
),
|
||||
|
||||
@@ -53,8 +53,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "4f83b6f"
|
||||
resolved-ref: "4f83b6feb92963f104f3f1f77a473a06aa4870f5"
|
||||
ref: cd0f67a
|
||||
resolved-ref: cd0f67a48e40188114800fae9a0f59cafe15b0f2
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||
source: git
|
||||
version: "1.0.4"
|
||||
|
||||
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 0.2.3
|
||||
version: 0.2.4
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
@@ -46,7 +46,7 @@ dependencies:
|
||||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
ref: 4f83b6f
|
||||
ref: cd0f67a
|
||||
appflowy_popover:
|
||||
path: packages/appflowy_popover
|
||||
|
||||
|
||||
@@ -8,6 +8,16 @@
|
||||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
_In_ wchar_t *command_line, _In_ int show_command)
|
||||
{
|
||||
HANDLE hMutexInstance = CreateMutex(NULL, TRUE, L"AppFlowyMutex");
|
||||
HWND handle = FindWindowA(NULL, "AppFlowy");
|
||||
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
||||
WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) };
|
||||
GetWindowPlacement(handle, &place);
|
||||
ShowWindow(handle, SW_NORMAL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Attach to console when present (e.g., 'flutter run') or create a
|
||||
// new console when running with a debugger.
|
||||
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent())
|
||||
@@ -43,5 +53,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
}
|
||||
|
||||
::CoUninitialize();
|
||||
ReleaseMutex(hMutexInstance);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
+10
-10
@@ -105,7 +105,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
|
||||
[[package]]
|
||||
name = "appflowy-integrate"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@@ -1030,7 +1030,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -1048,7 +1048,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-client-ws"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"collab-sync",
|
||||
@@ -1066,7 +1066,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-database"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1092,7 +1092,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-derive"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1104,7 +1104,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-document"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@@ -1122,7 +1122,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-folder"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -1142,7 +1142,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-persistence"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"chrono",
|
||||
@@ -1162,7 +1162,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-plugins"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1193,7 +1193,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-sync"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"collab",
|
||||
|
||||
@@ -201,7 +201,7 @@ export const EditRow = ({
|
||||
}}
|
||||
className={`relative flex h-[90%] w-[70%] flex-col gap-8 rounded-xl bg-white `}
|
||||
>
|
||||
<div onClick={() => onCloseClick()} className={'absolute top-1 right-1'}>
|
||||
<div onClick={() => onCloseClick()} className={'absolute right-1 top-1'}>
|
||||
<button className={'block h-8 w-8 rounded-lg text-shade-2 hover:bg-main-secondary'}>
|
||||
<CloseSvg></CloseSvg>
|
||||
</button>
|
||||
@@ -209,7 +209,7 @@ export const EditRow = ({
|
||||
|
||||
<div className={'flex h-full'}>
|
||||
<div className={'flex h-full flex-1 flex-col border-r border-shade-6 pb-4 pt-6'}>
|
||||
<div className={'pl-12 pb-4'}>
|
||||
<div className={'pb-4 pl-12'}>
|
||||
<button className={'flex items-center gap-2 p-4'}>
|
||||
<i className={'h-5 w-5'}>
|
||||
<ImageSvg></ImageSvg>
|
||||
@@ -229,7 +229,9 @@ export const EditRow = ({
|
||||
}`}
|
||||
>
|
||||
{cells
|
||||
.filter((cell) => databaseStore.fields[cell.cellIdentifier.fieldId].visible)
|
||||
.filter((cell) => {
|
||||
return databaseStore.fields[cell.cellIdentifier.fieldId]?.visible;
|
||||
})
|
||||
.map((cell, cellIndex) => (
|
||||
<EditCellWrapper
|
||||
index={cellIndex}
|
||||
|
||||
+28
-22
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { DatabaseController } from '$app/stores/effects/database/database_controller';
|
||||
import { databaseActions, DatabaseFieldMap, IDatabaseColumn } from '$app/stores/reducers/database/slice';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
@@ -8,6 +8,7 @@ import { RowInfo } from '$app/stores/effects/database/row/row_cache';
|
||||
import { ViewLayoutPB } from '@/services/backend';
|
||||
import { DatabaseGroupController } from '$app/stores/effects/database/group/group_controller';
|
||||
import { OnDragEndResponder } from 'react-beautiful-dnd';
|
||||
import { AsyncQueue } from '$app/utils/async_queue';
|
||||
|
||||
export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -24,25 +25,30 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
||||
return () => void c.dispose();
|
||||
}, [viewId]);
|
||||
|
||||
const loadFields = async (fieldInfos: readonly FieldInfo[]) => {
|
||||
const fields: DatabaseFieldMap = {};
|
||||
const columns: IDatabaseColumn[] = [];
|
||||
const loadFields = useCallback(
|
||||
async (fieldInfos: readonly FieldInfo[]) => {
|
||||
const fields: DatabaseFieldMap = {};
|
||||
const columns: IDatabaseColumn[] = [];
|
||||
for (const fieldInfo of fieldInfos) {
|
||||
const fieldPB = fieldInfo.field;
|
||||
columns.push({
|
||||
fieldId: fieldPB.id,
|
||||
sort: 'none',
|
||||
visible: fieldPB.visibility,
|
||||
});
|
||||
|
||||
for (const fieldInfo of fieldInfos) {
|
||||
const fieldPB = fieldInfo.field;
|
||||
columns.push({
|
||||
fieldId: fieldPB.id,
|
||||
sort: 'none',
|
||||
visible: fieldPB.visibility,
|
||||
});
|
||||
const field = await loadField(viewId, fieldInfo, dispatch);
|
||||
fields[field.fieldId] = field;
|
||||
}
|
||||
dispatch(databaseActions.updateFields({ fields }));
|
||||
dispatch(databaseActions.updateColumns({ columns }));
|
||||
},
|
||||
[viewId, dispatch]
|
||||
);
|
||||
|
||||
const field = await loadField(viewId, fieldInfo, dispatch);
|
||||
fields[field.fieldId] = field;
|
||||
}
|
||||
|
||||
dispatch(databaseActions.updateFields({ fields }));
|
||||
dispatch(databaseActions.updateColumns({ columns }));
|
||||
};
|
||||
const queue = useMemo(() => {
|
||||
return new AsyncQueue<readonly FieldInfo[]>(loadFields);
|
||||
}, [loadFields]);
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
@@ -53,7 +59,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
||||
setRows([...rowInfos]);
|
||||
},
|
||||
onFieldsChanged: (fieldInfos) => {
|
||||
void loadFields(fieldInfos);
|
||||
queue.enqueue(fieldInfos);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -76,7 +82,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
||||
return () => {
|
||||
void controller?.dispose();
|
||||
};
|
||||
}, [controller]);
|
||||
}, [controller, queue]);
|
||||
|
||||
const onNewRowClick = async (index: number) => {
|
||||
if (!groups) return;
|
||||
@@ -95,7 +101,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
||||
|
||||
if (source.droppableId === destination?.droppableId) {
|
||||
// move inside the block (group)
|
||||
await controller.exchangeRow(
|
||||
await controller.exchangeGroupRow(
|
||||
group.rows[source.index].id,
|
||||
destination.droppableId,
|
||||
group.rows[destination.index].id
|
||||
@@ -103,7 +109,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
||||
} else {
|
||||
// move to different block (group)
|
||||
if (!destination?.droppableId) return;
|
||||
await controller.moveRow(group.rows[source.index].id, destination.droppableId);
|
||||
await controller.moveGroupRow(group.rows[source.index].id, destination.droppableId);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Draggable } from 'react-beautiful-dnd';
|
||||
import { MouseEventHandler, useState } from 'react';
|
||||
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
||||
import { RowBackendService } from '$app/stores/effects/database/row/row_bd_svc';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
|
||||
@@ -52,8 +51,7 @@ export const BoardCard = ({
|
||||
|
||||
const onDeleteRowClick = async () => {
|
||||
setShowCardPopup(false);
|
||||
const svc = new RowBackendService(viewId);
|
||||
await svc.deleteRow(rowInfo.row.id);
|
||||
await controller.deleteRow(rowInfo.row.id);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -73,7 +71,7 @@ export const BoardCard = ({
|
||||
<div className={'flex flex-col gap-3'}>
|
||||
{cells
|
||||
.filter(
|
||||
(cell) => cell.fieldId !== groupByFieldId && databaseStore.fields[cell.cellIdentifier.fieldId].visible
|
||||
(cell) => cell.fieldId !== groupByFieldId && databaseStore.fields[cell.cellIdentifier.fieldId]?.visible
|
||||
)
|
||||
.map((cell, cellIndex) => (
|
||||
<BoardCell
|
||||
|
||||
+4
-5
@@ -1,6 +1,6 @@
|
||||
import { ShowMenuSvg } from '../../_shared/svg/ShowMenuSvg';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAppSelector } from '../../../stores/store';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export const Breadcrumbs = ({ menuHidden, onShowMenuClick }: { menuHidden: boolean; onShowMenuClick: () => void }) => {
|
||||
@@ -9,7 +9,6 @@ export const Breadcrumbs = ({ menuHidden, onShowMenuClick }: { menuHidden: boole
|
||||
const [activePageId, setActivePageId] = useState<string>('');
|
||||
const currentLocation = useLocation();
|
||||
const pagesStore = useAppSelector((state) => state.pages);
|
||||
const foldersStore = useAppSelector((state) => state.folders);
|
||||
|
||||
useEffect(() => {
|
||||
const { pathname } = currentLocation;
|
||||
@@ -20,10 +19,10 @@ export const Breadcrumbs = ({ menuHidden, onShowMenuClick }: { menuHidden: boole
|
||||
|
||||
useEffect(() => {
|
||||
const page = pagesStore.find((p) => p.id === activePageId);
|
||||
const folder = foldersStore.find((f) => f.id === page?.folderId);
|
||||
setFolderName(folder?.title ?? '');
|
||||
// const folder = foldersStore.find((f) => f.id === page?.parentPageId);
|
||||
// setFolderName(folder?.title ?? '');
|
||||
setPageName(page?.title ?? '');
|
||||
}, [pagesStore, foldersStore, activePageId]);
|
||||
}, [pagesStore, activePageId]);
|
||||
|
||||
return (
|
||||
<div className={'flex items-center'}>
|
||||
|
||||
-204
@@ -1,204 +0,0 @@
|
||||
import { foldersActions, IFolder } from '$app_reducers/folders/slice';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { IPage, pagesActions } from '$app_reducers/pages/slice';
|
||||
import { ViewLayoutPB } from '@/services/backend';
|
||||
import { AppBackendService } from '$app/stores/effects/folder/app/app_bd_svc';
|
||||
import { WorkspaceBackendService } from '$app/stores/effects/folder/workspace/workspace_bd_svc';
|
||||
|
||||
import { AppObserver } from '$app/stores/effects/folder/app/app_observer';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
|
||||
|
||||
export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
||||
const appDispatch = useAppDispatch();
|
||||
const workspace = useAppSelector((state) => state.workspace);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 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(`${INITIAL_FOLDER_HEIGHT}px`);
|
||||
|
||||
// Observers
|
||||
const appObserver = new AppObserver(folder.id);
|
||||
|
||||
// Backend services
|
||||
const appBackendService = new AppBackendService(folder.id);
|
||||
|
||||
useEffect(() => {
|
||||
void appObserver.subscribe({
|
||||
onViewsChanged: async () => {
|
||||
const result = await appBackendService.getAllViews();
|
||||
if (!result.ok) return;
|
||||
const views = result.val;
|
||||
const updatedPages: IPage[] = views.map((view) => ({
|
||||
id: view.id,
|
||||
folderId: view.parent_view_id,
|
||||
pageType: view.layout,
|
||||
title: view.name,
|
||||
}));
|
||||
appDispatch(pagesActions.didReceivePages({ pages: updatedPages, folderId: folder.id }));
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
// Unsubscribe when the component is unmounted.
|
||||
void appObserver.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (showPages) {
|
||||
setFolderHeight(`${INITIAL_FOLDER_HEIGHT + pages.length * PAGE_ITEM_HEIGHT}px`);
|
||||
}
|
||||
}, [pages]);
|
||||
|
||||
const onFolderNameClick = () => {
|
||||
if (showPages) {
|
||||
setFolderHeight(`${INITIAL_FOLDER_HEIGHT}px`);
|
||||
} else {
|
||||
setFolderHeight(`${INITIAL_FOLDER_HEIGHT + pages.length * PAGE_ITEM_HEIGHT}px`);
|
||||
}
|
||||
setShowPages(!showPages);
|
||||
};
|
||||
|
||||
const onFolderOptionsClick = () => {
|
||||
setShowFolderOptions(!showFolderOptions);
|
||||
};
|
||||
|
||||
const onNewPageClick = () => {
|
||||
setShowNewPageOptions(!showNewPageOptions);
|
||||
};
|
||||
|
||||
const startFolderRename = () => {
|
||||
closePopup();
|
||||
setShowRenamePopup(true);
|
||||
};
|
||||
|
||||
const changeFolderTitle = async (newTitle: string) => {
|
||||
await appBackendService.update({ name: newTitle });
|
||||
appDispatch(foldersActions.renameFolder({ id: folder.id, newTitle }));
|
||||
};
|
||||
|
||||
const closeRenamePopup = () => {
|
||||
setShowRenamePopup(false);
|
||||
};
|
||||
|
||||
const deleteFolder = async () => {
|
||||
closePopup();
|
||||
await appBackendService.delete();
|
||||
appDispatch(foldersActions.deleteFolder({ id: folder.id }));
|
||||
};
|
||||
|
||||
const duplicateFolder = async () => {
|
||||
closePopup();
|
||||
const workspaceBackendService = new WorkspaceBackendService(workspace.id ?? '');
|
||||
const newApp = await workspaceBackendService.createApp({
|
||||
name: folder.title,
|
||||
});
|
||||
appDispatch(foldersActions.addFolder({ id: newApp.id, title: folder.title }));
|
||||
};
|
||||
|
||||
const closePopup = () => {
|
||||
setShowFolderOptions(false);
|
||||
setShowNewPageOptions(false);
|
||||
};
|
||||
|
||||
const onAddNewDocumentPage = async () => {
|
||||
closePopup();
|
||||
const newView = await appBackendService.createView({
|
||||
name: 'New Document 1',
|
||||
layoutType: ViewLayoutPB.Document,
|
||||
});
|
||||
try {
|
||||
appDispatch(
|
||||
pagesActions.addPage({
|
||||
folderId: folder.id,
|
||||
pageType: ViewLayoutPB.Document,
|
||||
title: newView.name,
|
||||
id: newView.id,
|
||||
})
|
||||
);
|
||||
|
||||
setShowPages(true);
|
||||
|
||||
navigate(`/page/document/${newView.id}`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const onAddNewBoardPage = async () => {
|
||||
closePopup();
|
||||
const newView = await appBackendService.createView({
|
||||
name: 'New Board 1',
|
||||
layoutType: ViewLayoutPB.Board,
|
||||
});
|
||||
|
||||
setShowPages(true);
|
||||
|
||||
appDispatch(
|
||||
pagesActions.addPage({
|
||||
folderId: folder.id,
|
||||
pageType: ViewLayoutPB.Board,
|
||||
title: newView.name,
|
||||
id: newView.id,
|
||||
})
|
||||
);
|
||||
|
||||
navigate(`/page/board/${newView.id}`);
|
||||
};
|
||||
|
||||
const onAddNewGridPage = async () => {
|
||||
closePopup();
|
||||
const newView = await appBackendService.createView({
|
||||
name: 'New Grid 1',
|
||||
layoutType: ViewLayoutPB.Grid,
|
||||
});
|
||||
|
||||
setShowPages(true);
|
||||
|
||||
appDispatch(
|
||||
pagesActions.addPage({
|
||||
folderId: folder.id,
|
||||
pageType: ViewLayoutPB.Grid,
|
||||
title: newView.name,
|
||||
id: newView.id,
|
||||
})
|
||||
);
|
||||
|
||||
navigate(`/page/grid/${newView.id}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
appDispatch(foldersActions.setShowPages({ id: folder.id, showPages: showPages }));
|
||||
}, [showPages]);
|
||||
|
||||
return {
|
||||
showPages,
|
||||
onFolderNameClick,
|
||||
showFolderOptions,
|
||||
onFolderOptionsClick,
|
||||
showNewPageOptions,
|
||||
onNewPageClick,
|
||||
|
||||
showRenamePopup,
|
||||
startFolderRename,
|
||||
changeFolderTitle,
|
||||
closeRenamePopup,
|
||||
deleteFolder,
|
||||
duplicateFolder,
|
||||
|
||||
onAddNewDocumentPage,
|
||||
onAddNewBoardPage,
|
||||
onAddNewGridPage,
|
||||
|
||||
closePopup,
|
||||
folderHeight,
|
||||
};
|
||||
};
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
import { Details2Svg } from '../../_shared/svg/Details2Svg';
|
||||
import AddSvg from '../../_shared/svg/AddSvg';
|
||||
import { NavItemOptionsPopup } from './NavItemOptionsPopup';
|
||||
import { NewPagePopup } from './NewPagePopup';
|
||||
import { IFolder } from '$app_reducers/folders/slice';
|
||||
import { useFolderEvents } from './FolderItem.hooks';
|
||||
import { IPage } from '$app_reducers/pages/slice';
|
||||
import { PageItem } from './PageItem';
|
||||
import { Button } from '../../_shared/Button';
|
||||
import { RenamePopup } from './RenamePopup';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { DropDownShowSvg } from '../../_shared/svg/DropDownShowSvg';
|
||||
import { ANIMATION_DURATION } from '../../_shared/constants';
|
||||
|
||||
export const FolderItem = ({
|
||||
folder,
|
||||
pages,
|
||||
onPageClick,
|
||||
}: {
|
||||
folder: IFolder;
|
||||
pages: IPage[];
|
||||
onPageClick: (page: IPage) => void;
|
||||
}) => {
|
||||
const {
|
||||
showPages,
|
||||
onFolderNameClick,
|
||||
showFolderOptions,
|
||||
onFolderOptionsClick,
|
||||
showNewPageOptions,
|
||||
onNewPageClick,
|
||||
|
||||
showRenamePopup,
|
||||
startFolderRename,
|
||||
changeFolderTitle,
|
||||
closeRenamePopup,
|
||||
deleteFolder,
|
||||
duplicateFolder,
|
||||
|
||||
onAddNewDocumentPage,
|
||||
onAddNewBoardPage,
|
||||
onAddNewGridPage,
|
||||
|
||||
closePopup,
|
||||
folderHeight,
|
||||
} = useFolderEvents(folder, pages);
|
||||
|
||||
const [popupY, setPopupY] = useState(0);
|
||||
|
||||
const el = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (el.current) {
|
||||
const { top } = el.current.getBoundingClientRect();
|
||||
setPopupY(top);
|
||||
}
|
||||
}, [showFolderOptions, showNewPageOptions, showRenamePopup]);
|
||||
|
||||
return (
|
||||
<div ref={el}>
|
||||
<div
|
||||
className={`my-2 overflow-hidden transition-all`}
|
||||
style={{ height: folderHeight, transitionDuration: `${ANIMATION_DURATION}ms` }}
|
||||
>
|
||||
<div
|
||||
onClick={() => onFolderNameClick()}
|
||||
className={'flex cursor-pointer items-center justify-between rounded-lg px-4 py-2 hover:bg-surface-2'}
|
||||
>
|
||||
<button className={'flex min-w-0 flex-1 items-center'}>
|
||||
<i className={`mr-2 h-5 w-5 transition-transform duration-500 ${showPages && 'rotate-180'}`}>
|
||||
{pages.length > 0 && <DropDownShowSvg></DropDownShowSvg>}
|
||||
</i>
|
||||
<span className={'min-w-0 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap text-left'}>
|
||||
{folder.title}
|
||||
</span>
|
||||
</button>
|
||||
<div className={'flex items-center'}>
|
||||
<Button size={'box-small-transparent'} onClick={() => onFolderOptionsClick()}>
|
||||
<Details2Svg></Details2Svg>
|
||||
</Button>
|
||||
<Button size={'box-small-transparent'} onClick={() => onNewPageClick()}>
|
||||
<AddSvg></AddSvg>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{pages.map((page, index) => (
|
||||
<PageItem key={index} page={page} onPageClick={() => onPageClick(page)}></PageItem>
|
||||
))}
|
||||
</div>
|
||||
{showFolderOptions && (
|
||||
<NavItemOptionsPopup
|
||||
onRenameClick={() => startFolderRename()}
|
||||
onDeleteClick={() => deleteFolder()}
|
||||
onDuplicateClick={() => duplicateFolder()}
|
||||
onClose={() => closePopup()}
|
||||
top={popupY - 124 + 40}
|
||||
></NavItemOptionsPopup>
|
||||
)}
|
||||
{showNewPageOptions && (
|
||||
<NewPagePopup
|
||||
onDocumentClick={() => onAddNewDocumentPage()}
|
||||
onBoardClick={() => onAddNewBoardPage()}
|
||||
onGridClick={() => onAddNewGridPage()}
|
||||
onClose={() => closePopup()}
|
||||
top={popupY - 124 + 40}
|
||||
></NewPagePopup>
|
||||
)}
|
||||
{showRenamePopup && (
|
||||
<RenamePopup
|
||||
value={folder.title}
|
||||
onChange={(newTitle) => changeFolderTitle(newTitle)}
|
||||
onClose={closeRenamePopup}
|
||||
top={popupY - 124 + 40}
|
||||
></RenamePopup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { IPage, pagesActions } from '$app_reducers/pages/slice';
|
||||
import { ViewLayoutPB } from '@/services/backend';
|
||||
import { WorkspaceBackendService } from '$app/stores/effects/folder/workspace/workspace_bd_svc';
|
||||
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
|
||||
|
||||
import { ViewBackendService } from '$app/stores/effects/folder/view/view_bd_svc';
|
||||
import { ViewObserver } from '$app/stores/effects/folder/view/view_observer';
|
||||
|
||||
export const useNavItem = (page: IPage) => {
|
||||
const appDispatch = useAppDispatch();
|
||||
const workspace = useAppSelector((state) => state.workspace);
|
||||
const currentLocation = useLocation();
|
||||
const [activePageId, setActivePageId] = useState<string>('');
|
||||
const pages = useAppSelector((state) => state.pages);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Actions
|
||||
const [showPageOptions, setShowPageOptions] = useState(false);
|
||||
const [showNewPageOptions, setShowNewPageOptions] = useState(false);
|
||||
const [showRenamePopup, setShowRenamePopup] = useState(false);
|
||||
|
||||
// UI configurations
|
||||
const [folderHeight, setFolderHeight] = useState(`${INITIAL_FOLDER_HEIGHT}px`);
|
||||
|
||||
// backend
|
||||
const service = new ViewBackendService(page.id);
|
||||
const observer = new ViewObserver(page.id);
|
||||
|
||||
const loadInsidePages = async () => {
|
||||
const result = await service.getChildViews();
|
||||
if (!result.ok) return;
|
||||
const views = result.val;
|
||||
const updatedPages: IPage[] = views.map<IPage>((view) => ({
|
||||
parentPageId: page.id,
|
||||
id: view.id,
|
||||
pageType: view.layout,
|
||||
title: view.name,
|
||||
showPagesInside: false,
|
||||
}));
|
||||
appDispatch(pagesActions.addInsidePages({ currentPageId: page.id, insidePages: updatedPages }));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void loadInsidePages();
|
||||
void observer.subscribe({
|
||||
onChildViewsChanged: () => {
|
||||
void loadInsidePages();
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
// Unsubscribe when the component is unmounted.
|
||||
void observer.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const { pathname } = currentLocation;
|
||||
const parts = pathname.split('/');
|
||||
const pageId = parts[parts.length - 1];
|
||||
setActivePageId(pageId);
|
||||
}, [currentLocation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (page.showPagesInside) {
|
||||
setFolderHeight(`${PAGE_ITEM_HEIGHT + getChildCount(page) * PAGE_ITEM_HEIGHT}px`);
|
||||
} else {
|
||||
setFolderHeight(`${PAGE_ITEM_HEIGHT}px`);
|
||||
}
|
||||
}, [page, pages]);
|
||||
|
||||
// recursively get all unfolded child pages
|
||||
const getChildCount: (startPage: IPage) => number = (startPage: IPage) => {
|
||||
let count = 0;
|
||||
count = pages.filter((p) => p.parentPageId === startPage.id).length;
|
||||
pages
|
||||
.filter((p) => p.parentPageId === startPage.id)
|
||||
.forEach((p) => {
|
||||
if (p.showPagesInside) {
|
||||
count += getChildCount(p);
|
||||
}
|
||||
});
|
||||
return count;
|
||||
};
|
||||
|
||||
const onUnfoldClick = () => {
|
||||
appDispatch(pagesActions.toggleShowPages({ id: page.id }));
|
||||
};
|
||||
|
||||
const onPageOptionsClick = () => {
|
||||
setShowPageOptions(!showPageOptions);
|
||||
};
|
||||
|
||||
const startPageRename = () => {
|
||||
setShowRenamePopup(true);
|
||||
closePopup();
|
||||
};
|
||||
|
||||
const onNewPageClick = () => {
|
||||
setShowNewPageOptions(!showNewPageOptions);
|
||||
};
|
||||
|
||||
const changePageTitle = async (newTitle: string) => {
|
||||
await service.update({ name: newTitle });
|
||||
appDispatch(pagesActions.renamePage({ id: page.id, newTitle }));
|
||||
};
|
||||
|
||||
const closeRenamePopup = () => {
|
||||
setShowRenamePopup(false);
|
||||
};
|
||||
|
||||
const deletePage = async () => {
|
||||
closePopup();
|
||||
await service.delete();
|
||||
appDispatch(pagesActions.deletePage({ id: page.id }));
|
||||
};
|
||||
|
||||
const duplicatePage = async () => {
|
||||
closePopup();
|
||||
await service.duplicate();
|
||||
};
|
||||
|
||||
const closePopup = () => {
|
||||
setShowPageOptions(false);
|
||||
setShowNewPageOptions(false);
|
||||
};
|
||||
|
||||
const onPageClick = (eventPage: IPage) => {
|
||||
const pageTypeRoute = (() => {
|
||||
switch (eventPage.pageType) {
|
||||
case ViewLayoutPB.Document:
|
||||
return 'document';
|
||||
case ViewLayoutPB.Grid:
|
||||
return 'grid';
|
||||
case ViewLayoutPB.Board:
|
||||
return 'board';
|
||||
default:
|
||||
return 'document';
|
||||
}
|
||||
})();
|
||||
|
||||
navigate(`/page/${pageTypeRoute}/${eventPage.id}`);
|
||||
};
|
||||
|
||||
const onAddNewPage = async (pageType: ViewLayoutPB) => {
|
||||
closePopup();
|
||||
if (!workspace?.id) return;
|
||||
|
||||
let newPageName = '';
|
||||
let pageTypeRoute = '';
|
||||
|
||||
switch (pageType) {
|
||||
case ViewLayoutPB.Document:
|
||||
newPageName = 'Document Page 1';
|
||||
pageTypeRoute = 'document';
|
||||
break;
|
||||
case ViewLayoutPB.Grid:
|
||||
newPageName = 'Grid Page 1';
|
||||
pageTypeRoute = 'grid';
|
||||
break;
|
||||
case ViewLayoutPB.Board:
|
||||
newPageName = 'Board Page 1';
|
||||
pageTypeRoute = 'board';
|
||||
break;
|
||||
default:
|
||||
newPageName = 'Document Page 1';
|
||||
pageTypeRoute = 'document';
|
||||
break;
|
||||
}
|
||||
|
||||
const workspaceService = new WorkspaceBackendService(workspace.id);
|
||||
const newViewResult = await workspaceService.createView({
|
||||
name: newPageName,
|
||||
layoutType: pageType,
|
||||
parentViewId: page.id,
|
||||
});
|
||||
|
||||
if (newViewResult.ok) {
|
||||
const newView = newViewResult.val;
|
||||
if (!page.showPagesInside) {
|
||||
appDispatch(pagesActions.toggleShowPages({ id: page.id }));
|
||||
}
|
||||
|
||||
appDispatch(
|
||||
pagesActions.addPage({
|
||||
parentPageId: page.id,
|
||||
pageType,
|
||||
title: newView.name,
|
||||
id: newView.id,
|
||||
showPagesInside: false,
|
||||
})
|
||||
);
|
||||
|
||||
navigate(`/page/${pageTypeRoute}/${newView.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
onUnfoldClick,
|
||||
onNewPageClick,
|
||||
onPageOptionsClick,
|
||||
startPageRename,
|
||||
|
||||
changePageTitle,
|
||||
closeRenamePopup,
|
||||
closePopup,
|
||||
|
||||
showNewPageOptions,
|
||||
showPageOptions,
|
||||
showRenamePopup,
|
||||
|
||||
deletePage,
|
||||
duplicatePage,
|
||||
|
||||
onPageClick,
|
||||
|
||||
onAddNewPage,
|
||||
|
||||
folderHeight,
|
||||
activePageId,
|
||||
};
|
||||
};
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import { Details2Svg } from '../../_shared/svg/Details2Svg';
|
||||
import AddSvg from '../../_shared/svg/AddSvg';
|
||||
import { NavItemOptionsPopup } from './NavItemOptionsPopup';
|
||||
import { NewPagePopup } from './NewPagePopup';
|
||||
import { IPage } from '$app_reducers/pages/slice';
|
||||
import { Button } from '../../_shared/Button';
|
||||
import { RenamePopup } from './RenamePopup';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { DropDownShowSvg } from '../../_shared/svg/DropDownShowSvg';
|
||||
import { ANIMATION_DURATION, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
|
||||
import { useNavItem } from '$app/components/layout/NavigationPanel/NavItem.hooks';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { ViewLayoutPB } from '@/services/backend';
|
||||
|
||||
export const NavItem = ({ page }: { page: IPage }) => {
|
||||
const pages = useAppSelector((state) => state.pages);
|
||||
const {
|
||||
onUnfoldClick,
|
||||
onNewPageClick,
|
||||
onPageOptionsClick,
|
||||
startPageRename,
|
||||
|
||||
changePageTitle,
|
||||
closeRenamePopup,
|
||||
closePopup,
|
||||
|
||||
showNewPageOptions,
|
||||
showPageOptions,
|
||||
showRenamePopup,
|
||||
|
||||
deletePage,
|
||||
duplicatePage,
|
||||
|
||||
onAddNewPage,
|
||||
|
||||
folderHeight,
|
||||
activePageId,
|
||||
|
||||
onPageClick,
|
||||
} = useNavItem(page);
|
||||
|
||||
const [popupY, setPopupY] = useState(0);
|
||||
|
||||
const el = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (el.current) {
|
||||
const { top } = el.current.getBoundingClientRect();
|
||||
setPopupY(top);
|
||||
}
|
||||
}, [showPageOptions, showNewPageOptions, showRenamePopup]);
|
||||
|
||||
return (
|
||||
<div ref={el}>
|
||||
<div
|
||||
className={`overflow-hidden transition-all`}
|
||||
style={{ height: folderHeight, transitionDuration: `${ANIMATION_DURATION}ms` }}
|
||||
>
|
||||
<div
|
||||
style={{ height: PAGE_ITEM_HEIGHT }}
|
||||
className={`flex cursor-pointer items-center justify-between rounded-lg px-4 hover:bg-surface-2 ${
|
||||
activePageId === page.id ? 'bg-surface-2' : ''
|
||||
}`}
|
||||
>
|
||||
<div className={'flex h-full min-w-0 flex-1 items-center'}>
|
||||
<button
|
||||
onClick={() => onUnfoldClick()}
|
||||
className={`mr-2 h-5 w-5 transition-transform duration-200 ${page.showPagesInside && 'rotate-180'}`}
|
||||
>
|
||||
<DropDownShowSvg></DropDownShowSvg>
|
||||
</button>
|
||||
<div
|
||||
onClick={() => onPageClick(page)}
|
||||
className={
|
||||
'flex h-full min-w-0 flex-1 items-center overflow-hidden overflow-ellipsis whitespace-nowrap text-left'
|
||||
}
|
||||
>
|
||||
{page.title}
|
||||
</div>
|
||||
</div>
|
||||
<div className={'flex items-center'}>
|
||||
<Button size={'box-small-transparent'} onClick={() => onPageOptionsClick()}>
|
||||
<Details2Svg></Details2Svg>
|
||||
</Button>
|
||||
<Button size={'box-small-transparent'} onClick={() => onNewPageClick()}>
|
||||
<AddSvg></AddSvg>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'pl-4'}>
|
||||
{useMemo(() => pages.filter((insidePage) => insidePage.parentPageId === page.id), [pages, page]).map(
|
||||
(insidePage, insideIndex) => (
|
||||
<NavItem key={insideIndex} page={insidePage}></NavItem>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showPageOptions && (
|
||||
<NavItemOptionsPopup
|
||||
onRenameClick={() => startPageRename()}
|
||||
onDeleteClick={() => deletePage()}
|
||||
onDuplicateClick={() => duplicatePage()}
|
||||
onClose={() => closePopup()}
|
||||
top={popupY - 124 + 40}
|
||||
></NavItemOptionsPopup>
|
||||
)}
|
||||
{showNewPageOptions && (
|
||||
<NewPagePopup
|
||||
onDocumentClick={() => onAddNewPage(ViewLayoutPB.Document)}
|
||||
onBoardClick={() => onAddNewPage(ViewLayoutPB.Board)}
|
||||
onGridClick={() => onAddNewPage(ViewLayoutPB.Grid)}
|
||||
onClose={() => closePopup()}
|
||||
top={popupY - 124 + 40}
|
||||
></NewPagePopup>
|
||||
)}
|
||||
{showRenamePopup && (
|
||||
<RenamePopup
|
||||
value={page.title}
|
||||
onChange={(newTitle) => changePageTitle(newTitle)}
|
||||
onClose={closeRenamePopup}
|
||||
top={popupY - 124 + 40}
|
||||
></RenamePopup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-27
@@ -1,17 +1,10 @@
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { IPage } from '$app_reducers/pages/slice';
|
||||
import { ViewLayoutPB } from '@/services/backend';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const useNavigationPanelHooks = function () {
|
||||
const folders = useAppSelector((state) => state.folders);
|
||||
const pages = useAppSelector((state) => state.pages);
|
||||
const width = useAppSelector((state) => state.navigationWidth);
|
||||
const [menuHidden, setMenuHidden] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onHideMenuClick = () => {
|
||||
setMenuHidden(true);
|
||||
};
|
||||
@@ -20,28 +13,8 @@ export const useNavigationPanelHooks = function () {
|
||||
setMenuHidden(false);
|
||||
};
|
||||
|
||||
const onPageClick = (page: IPage) => {
|
||||
const pageTypeRoute = (() => {
|
||||
switch (page.pageType) {
|
||||
case ViewLayoutPB.Document:
|
||||
return 'document';
|
||||
case ViewLayoutPB.Grid:
|
||||
return 'grid';
|
||||
case ViewLayoutPB.Board:
|
||||
return 'board';
|
||||
default:
|
||||
return 'document';
|
||||
}
|
||||
})();
|
||||
|
||||
navigate(`/page/${pageTypeRoute}/${page.id}`);
|
||||
};
|
||||
|
||||
return {
|
||||
width,
|
||||
folders,
|
||||
pages,
|
||||
onPageClick,
|
||||
menuHidden,
|
||||
onHideMenuClick,
|
||||
onShowMenuClick,
|
||||
|
||||
+13
-73
@@ -1,40 +1,27 @@
|
||||
import { WorkspaceUser } from '../WorkspaceUser';
|
||||
import { AppLogo } from '../AppLogo';
|
||||
import { FolderItem } from './FolderItem';
|
||||
import { TrashButton } from './TrashButton';
|
||||
import { NewFolderButton } from './NewFolderButton';
|
||||
import { NewViewButton } from './NewViewButton';
|
||||
import { NavigationResizer } from './NavigationResizer';
|
||||
import { IFolder } from '$app_reducers/folders/slice';
|
||||
import { IPage } from '$app_reducers/pages/slice';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import {
|
||||
ANIMATION_DURATION,
|
||||
FOLDER_MARGIN,
|
||||
INITIAL_FOLDER_HEIGHT,
|
||||
NAV_PANEL_MINIMUM_WIDTH,
|
||||
PAGE_ITEM_HEIGHT,
|
||||
} from '../../_shared/constants';
|
||||
import { NavItem } from '$app/components/layout/NavigationPanel/NavItem';
|
||||
import { ANIMATION_DURATION, NAV_PANEL_MINIMUM_WIDTH, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
|
||||
|
||||
export const NavigationPanel = ({
|
||||
onHideMenuClick,
|
||||
menuHidden,
|
||||
width,
|
||||
folders,
|
||||
pages,
|
||||
onPageClick,
|
||||
}: {
|
||||
onHideMenuClick: () => void;
|
||||
menuHidden: boolean;
|
||||
width: number;
|
||||
folders: IFolder[];
|
||||
pages: IPage[];
|
||||
onPageClick: (page: IPage) => void;
|
||||
}) => {
|
||||
const el = useRef<HTMLDivElement>(null);
|
||||
const foldersStore = useAppSelector((state) => state.folders);
|
||||
const pagesStore = useAppSelector((state) => state.pages);
|
||||
const pages = useAppSelector((state) => state.pages);
|
||||
const workspace = useAppSelector((state) => state.workspace);
|
||||
const [activePageId, setActivePageId] = useState<string>('');
|
||||
const currentLocation = useLocation();
|
||||
const [maxHeight, setMaxHeight] = useState(0);
|
||||
@@ -47,44 +34,8 @@ export const NavigationPanel = ({
|
||||
}, [currentLocation]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (!el.current) return;
|
||||
if (!activePageId?.length) return;
|
||||
const activePage = pagesStore.find((page) => page.id === activePageId);
|
||||
if (!activePage) return;
|
||||
|
||||
const folderIndex = foldersStore.findIndex((folder) => folder.id === activePage.folderId);
|
||||
if (folderIndex === -1) return;
|
||||
|
||||
let height = 0;
|
||||
for (let i = 0; i < folderIndex; i++) {
|
||||
height += INITIAL_FOLDER_HEIGHT + FOLDER_MARGIN;
|
||||
if (foldersStore[i].showPages) {
|
||||
height += pagesStore.filter((p) => p.folderId === foldersStore[i].id).length * PAGE_ITEM_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
height += INITIAL_FOLDER_HEIGHT + FOLDER_MARGIN / 2;
|
||||
|
||||
const pageIndex = pagesStore
|
||||
.filter((p) => p.folderId === foldersStore[folderIndex].id)
|
||||
.findIndex((p) => p.id === activePageId);
|
||||
for (let i = 0; i <= pageIndex; i++) {
|
||||
height += PAGE_ITEM_HEIGHT;
|
||||
}
|
||||
|
||||
const elHeight = el.current.getBoundingClientRect().height;
|
||||
const scrollTop = el.current.scrollTop;
|
||||
|
||||
if (scrollTop + elHeight < height || scrollTop > height) {
|
||||
el.current.scrollTo({ top: height - elHeight, behavior: 'smooth' });
|
||||
}
|
||||
}, ANIMATION_DURATION);
|
||||
}, [activePageId]);
|
||||
|
||||
useEffect(() => {
|
||||
setMaxHeight(foldersStore.length * (INITIAL_FOLDER_HEIGHT + FOLDER_MARGIN) + pagesStore.length * PAGE_ITEM_HEIGHT);
|
||||
}, [foldersStore, pagesStore]);
|
||||
setMaxHeight(pages.length * PAGE_ITEM_HEIGHT);
|
||||
}, [pages]);
|
||||
|
||||
const scrollDown = () => {
|
||||
setTimeout(() => {
|
||||
@@ -113,7 +64,7 @@ export const NavigationPanel = ({
|
||||
}}
|
||||
ref={el}
|
||||
>
|
||||
<WorkspaceApps folders={folders} pages={pages} onPageClick={onPageClick} />
|
||||
<WorkspaceApps pages={pages.filter((p) => p.parentPageId === workspace.id)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -130,8 +81,8 @@ export const NavigationPanel = ({
|
||||
<TrashButton></TrashButton>
|
||||
</div>
|
||||
|
||||
{/*New Folder Button*/}
|
||||
<NewFolderButton scrollDown={scrollDown}></NewFolderButton>
|
||||
{/*New Root View Button*/}
|
||||
<NewViewButton scrollDown={scrollDown}></NewViewButton>
|
||||
</div>
|
||||
</div>
|
||||
<NavigationResizer minWidth={NAV_PANEL_MINIMUM_WIDTH}></NavigationResizer>
|
||||
@@ -139,21 +90,10 @@ export const NavigationPanel = ({
|
||||
);
|
||||
};
|
||||
|
||||
type AppsContext = {
|
||||
folders: IFolder[];
|
||||
pages: IPage[];
|
||||
onPageClick: (page: IPage) => void;
|
||||
};
|
||||
|
||||
const WorkspaceApps: React.FC<AppsContext> = ({ folders, pages, onPageClick }) => (
|
||||
const WorkspaceApps: React.FC<{ pages: IPage[] }> = ({ pages }) => (
|
||||
<>
|
||||
{folders.map((folder, index) => (
|
||||
<FolderItem
|
||||
key={index}
|
||||
folder={folder}
|
||||
pages={pages.filter((page) => page.folderId === folder.id)}
|
||||
onPageClick={onPageClick}
|
||||
></FolderItem>
|
||||
{pages.map((page, index) => (
|
||||
<NavItem key={index} page={page}></NavItem>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { foldersActions } from '$app_reducers/folders/slice';
|
||||
import { WorkspaceBackendService } from '$app/stores/effects/folder/workspace/workspace_bd_svc';
|
||||
|
||||
export const useNewFolder = () => {
|
||||
const appDispatch = useAppDispatch();
|
||||
const workspace = useAppSelector((state) => state.workspace);
|
||||
const workspaceBackendService = new WorkspaceBackendService(workspace.id ?? '');
|
||||
|
||||
const onNewFolder = async () => {
|
||||
const newApp = await workspaceBackendService.createApp({
|
||||
name: 'New Folder 1',
|
||||
});
|
||||
appDispatch(foldersActions.addFolder({ id: newApp.id, title: newApp.name }));
|
||||
};
|
||||
|
||||
return {
|
||||
onNewFolder,
|
||||
};
|
||||
};
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { WorkspaceBackendService } from '$app/stores/effects/folder/workspace/workspace_bd_svc';
|
||||
import { ViewLayoutPB } from '@/services/backend';
|
||||
import { pagesActions } from '$app_reducers/pages/slice';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const useNewRootView = () => {
|
||||
const appDispatch = useAppDispatch();
|
||||
const workspace = useAppSelector((state) => state.workspace);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onNewRootView = async () => {
|
||||
if (!workspace.id) return;
|
||||
const workspaceBackendService = new WorkspaceBackendService(workspace.id);
|
||||
|
||||
// in future should show options for new page type
|
||||
const defaultType = ViewLayoutPB.Document;
|
||||
const defaultName = 'Document Page 1';
|
||||
const defaultRoute = 'document';
|
||||
|
||||
const result = await workspaceBackendService.createView({
|
||||
parentViewId: workspace.id,
|
||||
layoutType: defaultType,
|
||||
name: defaultName,
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
const newView = result.val;
|
||||
appDispatch(
|
||||
pagesActions.addPage({
|
||||
parentPageId: workspace.id,
|
||||
id: newView.id,
|
||||
title: newView.name,
|
||||
showPagesInside: false,
|
||||
pageType: defaultType,
|
||||
})
|
||||
);
|
||||
navigate(`/page/${defaultRoute}/${newView.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
onNewRootView,
|
||||
};
|
||||
};
|
||||
+5
-5
@@ -1,13 +1,13 @@
|
||||
import AddSvg from '../../_shared/svg/AddSvg';
|
||||
import { useNewFolder } from './NewFolderButton.hooks';
|
||||
import { useNewRootView } from './NewViewButton.hooks';
|
||||
|
||||
export const NewFolderButton = ({ scrollDown }: { scrollDown: () => void }) => {
|
||||
const { onNewFolder } = useNewFolder();
|
||||
export const NewViewButton = ({ scrollDown }: { scrollDown: () => void }) => {
|
||||
const { onNewRootView } = useNewRootView();
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
void onNewFolder();
|
||||
void onNewRootView();
|
||||
scrollDown();
|
||||
}}
|
||||
className={'flex h-[50px] w-full items-center px-6 hover:bg-surface-2'}
|
||||
@@ -17,7 +17,7 @@ export const NewFolderButton = ({ scrollDown }: { scrollDown: () => void }) => {
|
||||
<AddSvg></AddSvg>
|
||||
</div>
|
||||
</div>
|
||||
<span>New Folder</span>
|
||||
<span>New View</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
import { IPage, pagesActions } from '$app_reducers/pages/slice';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ViewBackendService } from '$app/stores/effects/folder/view/view_bd_svc';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { ViewPB } from '@/services/backend';
|
||||
|
||||
export const usePageEvents = (page: IPage) => {
|
||||
const appDispatch = useAppDispatch();
|
||||
const [showPageOptions, setShowPageOptions] = useState(false);
|
||||
const [showRenamePopup, setShowRenamePopup] = useState(false);
|
||||
const [activePageId, setActivePageId] = useState<string>('');
|
||||
const currentLocation = useLocation();
|
||||
const viewBackendService: ViewBackendService = new ViewBackendService(page.id);
|
||||
|
||||
useEffect(() => {
|
||||
const { pathname } = currentLocation;
|
||||
const parts = pathname.split('/');
|
||||
const pageId = parts[parts.length - 1];
|
||||
setActivePageId(pageId);
|
||||
}, [currentLocation]);
|
||||
|
||||
const onPageOptionsClick = () => {
|
||||
setShowPageOptions(!showPageOptions);
|
||||
};
|
||||
|
||||
const startPageRename = () => {
|
||||
setShowRenamePopup(true);
|
||||
closePopup();
|
||||
};
|
||||
|
||||
const changePageTitle = async (newTitle: string) => {
|
||||
await viewBackendService.update({ name: newTitle });
|
||||
appDispatch(pagesActions.renamePage({ id: page.id, newTitle }));
|
||||
};
|
||||
|
||||
const deletePage = async () => {
|
||||
closePopup();
|
||||
await viewBackendService.delete();
|
||||
appDispatch(pagesActions.deletePage({ id: page.id }));
|
||||
};
|
||||
|
||||
const duplicatePage = async () => {
|
||||
closePopup();
|
||||
await viewBackendService.duplicate(ViewPB.fromObject(page));
|
||||
};
|
||||
|
||||
const closePopup = () => {
|
||||
setShowPageOptions(false);
|
||||
};
|
||||
|
||||
const closeRenamePopup = () => {
|
||||
setShowRenamePopup(false);
|
||||
};
|
||||
|
||||
return {
|
||||
showPageOptions,
|
||||
onPageOptionsClick,
|
||||
showRenamePopup,
|
||||
startPageRename,
|
||||
changePageTitle,
|
||||
deletePage,
|
||||
duplicatePage,
|
||||
closePopup,
|
||||
closeRenamePopup,
|
||||
activePageId,
|
||||
};
|
||||
};
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
import { DocumentSvg } from '../../_shared/svg/DocumentSvg';
|
||||
import { BoardSvg } from '../../_shared/svg/BoardSvg';
|
||||
import { GridSvg } from '../../_shared/svg/GridSvg';
|
||||
import { Details2Svg } from '../../_shared/svg/Details2Svg';
|
||||
import { NavItemOptionsPopup } from './NavItemOptionsPopup';
|
||||
import { IPage } from '$app_reducers/pages/slice';
|
||||
import { Button } from '../../_shared/Button';
|
||||
import { usePageEvents } from './PageItem.hooks';
|
||||
import { RenamePopup } from './RenamePopup';
|
||||
import { ViewLayoutPB } from '@/services/backend';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { PAGE_ITEM_HEIGHT } from '../../_shared/constants';
|
||||
|
||||
export const PageItem = ({ page, onPageClick }: { page: IPage; onPageClick: () => void }) => {
|
||||
const {
|
||||
showPageOptions,
|
||||
onPageOptionsClick,
|
||||
showRenamePopup,
|
||||
startPageRename,
|
||||
changePageTitle,
|
||||
deletePage,
|
||||
duplicatePage,
|
||||
closePopup,
|
||||
closeRenamePopup,
|
||||
activePageId,
|
||||
} = usePageEvents(page);
|
||||
|
||||
const el = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [popupY, setPopupY] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (el.current) {
|
||||
const { top } = el.current.getBoundingClientRect();
|
||||
setPopupY(top);
|
||||
}
|
||||
}, [showPageOptions, showRenamePopup]);
|
||||
|
||||
return (
|
||||
<div ref={el}>
|
||||
<div
|
||||
onClick={() => onPageClick()}
|
||||
className={`flex cursor-pointer items-center justify-between rounded-lg pl-8 pr-4 hover:bg-surface-2 ${
|
||||
activePageId === page.id ? 'bg-surface-2' : ''
|
||||
}`}
|
||||
style={{ height: PAGE_ITEM_HEIGHT }}
|
||||
>
|
||||
<button className={'flex min-w-0 flex-1 items-center'}>
|
||||
<i className={'ml-1 mr-1 h-[16px] w-[16px]'}>
|
||||
{page.pageType === ViewLayoutPB.Document && <DocumentSvg></DocumentSvg>}
|
||||
{page.pageType === ViewLayoutPB.Board && <BoardSvg></BoardSvg>}
|
||||
{page.pageType === ViewLayoutPB.Grid && <GridSvg></GridSvg>}
|
||||
</i>
|
||||
<span className={'ml-2 min-w-0 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap text-left'}>
|
||||
{page.title}
|
||||
</span>
|
||||
</button>
|
||||
<div className={'flex items-center'}>
|
||||
<Button size={'box-small-transparent'} onClick={() => onPageOptionsClick()}>
|
||||
<Details2Svg></Details2Svg>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{showPageOptions && (
|
||||
<NavItemOptionsPopup
|
||||
onRenameClick={() => startPageRename()}
|
||||
onDeleteClick={() => deletePage()}
|
||||
onDuplicateClick={() => duplicatePage()}
|
||||
onClose={() => closePopup()}
|
||||
top={popupY - 124 + 40}
|
||||
></NavItemOptionsPopup>
|
||||
)}
|
||||
{showRenamePopup && (
|
||||
<RenamePopup
|
||||
value={page.title}
|
||||
onChange={(newTitle) => changePageTitle(newTitle)}
|
||||
onClose={closeRenamePopup}
|
||||
top={popupY - 124 + 40}
|
||||
></RenamePopup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,31 +1,17 @@
|
||||
import React, { ReactNode, useEffect } from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { NavigationPanel } from './NavigationPanel/NavigationPanel';
|
||||
import { MainPanel } from './MainPanel';
|
||||
import { useNavigationPanelHooks } from './NavigationPanel/NavigationPanel.hooks';
|
||||
import { useWorkspace } from './Workspace.hooks';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
|
||||
export const Screen = ({ children }: { children: ReactNode }) => {
|
||||
const currentUser = useAppSelector((state) => state.currentUser);
|
||||
const { loadWorkspaceItems } = useWorkspace();
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
await loadWorkspaceItems();
|
||||
})();
|
||||
}, [currentUser.isAuthenticated]);
|
||||
useWorkspace();
|
||||
|
||||
const { width, folders, pages, onPageClick, onHideMenuClick, onShowMenuClick, menuHidden } = useNavigationPanelHooks();
|
||||
const { width, onHideMenuClick, onShowMenuClick, menuHidden } = useNavigationPanelHooks();
|
||||
|
||||
return (
|
||||
<div className='flex h-screen w-screen bg-white text-black'>
|
||||
<NavigationPanel
|
||||
onHideMenuClick={onHideMenuClick}
|
||||
width={width}
|
||||
folders={folders}
|
||||
pages={pages}
|
||||
onPageClick={onPageClick}
|
||||
menuHidden={menuHidden}
|
||||
></NavigationPanel>
|
||||
<NavigationPanel onHideMenuClick={onHideMenuClick} width={width} menuHidden={menuHidden}></NavigationPanel>
|
||||
|
||||
<MainPanel left={width} menuHidden={menuHidden} onShowMenuClick={onShowMenuClick}>
|
||||
{children}
|
||||
|
||||
@@ -1,52 +1,69 @@
|
||||
import { foldersActions } from '$app_reducers/folders/slice';
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { pagesActions } from '$app_reducers/pages/slice';
|
||||
import { IPage, pagesActions } from '$app_reducers/pages/slice';
|
||||
import { workspaceActions } from '$app_reducers/workspace/slice';
|
||||
import { UserBackendService } from '$app/stores/effects/user/user_bd_svc';
|
||||
import { AppBackendService } from '$app/stores/effects/folder/app/app_bd_svc';
|
||||
import { Log } from '$app/utils/log';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { WorkspaceBackendService } from '$app/stores/effects/folder/workspace/workspace_bd_svc';
|
||||
|
||||
export const useWorkspace = () => {
|
||||
const currentUser = useAppSelector((state) => state.currentUser);
|
||||
|
||||
const appDispatch = useAppDispatch();
|
||||
|
||||
const userBackendService: UserBackendService = new UserBackendService(currentUser.id ?? 0);
|
||||
const [userService, setUserService] = useState<UserBackendService | null>(null);
|
||||
const [workspaceService, setWorkspaceService] = useState<WorkspaceBackendService | null>(null);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
|
||||
const loadWorkspaceItems = async () => {
|
||||
try {
|
||||
const workspaceSettingPB = await userBackendService.getCurrentWorkspace();
|
||||
const workspace = workspaceSettingPB.workspace;
|
||||
appDispatch(workspaceActions.updateWorkspace({ id: workspace.id, name: workspace.name }));
|
||||
appDispatch(foldersActions.clearFolders());
|
||||
appDispatch(pagesActions.clearPages());
|
||||
|
||||
const apps = workspace.views;
|
||||
for (const app of apps) {
|
||||
appDispatch(foldersActions.addFolder({ id: app.id, title: app.name }));
|
||||
const service = new AppBackendService(app.id);
|
||||
const result = await service.getAllViews();
|
||||
if (result.ok) {
|
||||
for (const view of result.val) {
|
||||
appDispatch(
|
||||
pagesActions.addPage({ folderId: app.id, id: view.id, pageType: view.layout, title: view.name })
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Log.error('Failed to get views, folderId: ' + app.id);
|
||||
}
|
||||
}
|
||||
} catch (e1) {
|
||||
// create workspace for first start
|
||||
const workspace = await userBackendService.createWorkspace({ name: 'New Workspace', desc: '' });
|
||||
appDispatch(workspaceActions.updateWorkspace({ id: workspace.id, name: workspace.name }));
|
||||
|
||||
appDispatch(foldersActions.clearFolders());
|
||||
appDispatch(pagesActions.clearPages());
|
||||
useEffect(() => {
|
||||
if (currentUser.id) {
|
||||
setUserService(new UserBackendService(currentUser.id));
|
||||
}
|
||||
};
|
||||
}, [currentUser]);
|
||||
|
||||
return {
|
||||
loadWorkspaceItems,
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!userService) return;
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const workspaceSettingPB = await userService.getCurrentWorkspace();
|
||||
const workspace = workspaceSettingPB.workspace;
|
||||
appDispatch(workspaceActions.updateWorkspace({ id: workspace.id, name: workspace.name }));
|
||||
appDispatch(foldersActions.clearFolders());
|
||||
appDispatch(pagesActions.clearPages());
|
||||
|
||||
setWorkspaceService(new WorkspaceBackendService(workspace.id));
|
||||
} catch (e1) {
|
||||
// create workspace for first start
|
||||
const workspace = await userService.createWorkspace({ name: 'New Workspace', desc: '' });
|
||||
appDispatch(workspaceActions.updateWorkspace({ id: workspace.id, name: workspace.name }));
|
||||
|
||||
appDispatch(foldersActions.clearFolders());
|
||||
appDispatch(pagesActions.clearPages());
|
||||
}
|
||||
})();
|
||||
}, [userService]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspaceService) return;
|
||||
void (async () => {
|
||||
const rootViews = await workspaceService.getAllViews();
|
||||
if (rootViews.ok) {
|
||||
appDispatch(
|
||||
pagesActions.addInsidePages({
|
||||
currentPageId: workspaceService.workspaceId,
|
||||
insidePages: rootViews.val.map<IPage>((v) => ({
|
||||
id: v.id,
|
||||
title: v.name,
|
||||
pageType: v.layout,
|
||||
showPagesInside: false,
|
||||
parentPageId: workspaceService.workspaceId,
|
||||
})),
|
||||
})
|
||||
);
|
||||
setIsReady(true);
|
||||
}
|
||||
})();
|
||||
}, [workspaceService]);
|
||||
|
||||
return {};
|
||||
};
|
||||
@@ -1,12 +1,12 @@
|
||||
import {
|
||||
FieldType,
|
||||
FlowyError,
|
||||
SingleSelectTypeOptionPB,
|
||||
ViewLayoutPB,
|
||||
ViewPB,
|
||||
WorkspaceSettingPB,
|
||||
} from '../../../services/backend';
|
||||
import { FolderEventGetCurrentWorkspace } from '../../../services/backend/events/flowy-folder2';
|
||||
import { AppBackendService } from '../../stores/effects/folder/app/app_bd_svc';
|
||||
import { DatabaseController } from '../../stores/effects/database/database_controller';
|
||||
import { RowInfo } from '../../stores/effects/database/row/row_cache';
|
||||
import { RowController } from '../../stores/effects/database/row/row_controller';
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
TextCellController,
|
||||
URLCellController,
|
||||
} from '../../stores/effects/database/cell/controller_builder';
|
||||
import { None, Option, Some } from 'ts-results';
|
||||
import { None, Ok, Option, Result, 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';
|
||||
@@ -27,13 +27,20 @@ import { TypeOptionController } from '../../stores/effects/database/field/type_o
|
||||
import { makeSingleSelectTypeOptionContext } from '../../stores/effects/database/field/type_option/type_option_context';
|
||||
import { SelectOptionBackendService } from '../../stores/effects/database/cell/select_option_bd_svc';
|
||||
import { Log } from '$app/utils/log';
|
||||
import { ViewBackendService } from '$app/stores/effects/folder/view/view_bd_svc';
|
||||
import { WorkspaceBackendService } from '$app/stores/effects/folder/workspace/workspace_bd_svc';
|
||||
|
||||
// Create a database view for specific layout type
|
||||
// Do not use it production code. Just for testing
|
||||
export async function createTestDatabaseView(layout: ViewLayoutPB): Promise<ViewPB> {
|
||||
const workspaceSetting: WorkspaceSettingPB = await FolderEventGetCurrentWorkspace().then((result) => result.unwrap());
|
||||
const appService = new AppBackendService(workspaceSetting.workspace.id);
|
||||
return await appService.createView({ name: 'New Grid', layoutType: layout });
|
||||
const wsSvc = new WorkspaceBackendService(workspaceSetting.workspace.id);
|
||||
const viewRes = await wsSvc.createView({ name: 'New Grid', layoutType: layout });
|
||||
if (viewRes.ok) {
|
||||
return viewRes.val;
|
||||
} else {
|
||||
throw Error(viewRes.val.msg);
|
||||
}
|
||||
}
|
||||
|
||||
export async function openTestDatabase(viewId: string): Promise<DatabaseController> {
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { ViewLayoutPB, WorkspaceSettingPB } from '@/services/backend';
|
||||
import { FolderEventGetCurrentWorkspace } from '@/services/backend/events/flowy-folder2';
|
||||
import { AppBackendService } from '$app/stores/effects/folder/app/app_bd_svc';
|
||||
import {WorkspaceBackendService} from "$app/stores/effects/folder/workspace/workspace_bd_svc";
|
||||
|
||||
export async function createTestDocument() {
|
||||
const workspaceSetting: WorkspaceSettingPB = await FolderEventGetCurrentWorkspace().then((result) => result.unwrap());
|
||||
const app = workspaceSetting.workspace.views[0];
|
||||
const appService = new AppBackendService(app.id);
|
||||
return await appService.createView({ name: 'New Document', layoutType: ViewLayoutPB.Document });
|
||||
const appService = new WorkspaceBackendService(workspaceSetting.workspace.id);
|
||||
const result = await appService.createView({ name: 'New Document', layoutType: ViewLayoutPB.Document });
|
||||
if (result.ok) {
|
||||
return result.val;
|
||||
}
|
||||
else {
|
||||
throw Error(result.val.msg);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
TestMoveKanbanBoardRow,
|
||||
} from './TestGroup';
|
||||
import { TestCreateDocument } from './TestDocument';
|
||||
import { TestCreateViews } from '$app/components/tests/TestFolder';
|
||||
|
||||
export const TestAPI = () => {
|
||||
return (
|
||||
@@ -62,6 +63,8 @@ export const TestAPI = () => {
|
||||
<TestMoveKanbanBoardColumn></TestMoveKanbanBoardColumn>
|
||||
<TestCreateKanbanBoardColumn></TestCreateKanbanBoardColumn>
|
||||
<TestCreateDocument></TestCreateDocument>
|
||||
{/*Folders*/}
|
||||
<TestCreateViews></TestCreateViews>
|
||||
</ul>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { UserBackendService } from '$app/stores/effects/user/user_bd_svc';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { WorkspaceBackendService } from '$app/stores/effects/folder/workspace/workspace_bd_svc';
|
||||
import { ViewLayoutPB, ViewPB } from '@/services/backend';
|
||||
|
||||
const testCreateFolder = async (userId?: number) => {
|
||||
if (!userId) {
|
||||
console.log('user is not logged in');
|
||||
return;
|
||||
}
|
||||
console.log('test create views');
|
||||
const userBackendService: UserBackendService = new UserBackendService(userId);
|
||||
const workspaces = await userBackendService.getWorkspaces();
|
||||
if (workspaces.ok) {
|
||||
console.log('workspaces: ', workspaces.val.toObject());
|
||||
}
|
||||
const currentWorkspace = await userBackendService.getCurrentWorkspace();
|
||||
|
||||
const workspaceService = new WorkspaceBackendService(currentWorkspace.workspace.id);
|
||||
const rootViews: ViewPB[] = [];
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const result = await workspaceService.createView({
|
||||
name: `test board ${i}`,
|
||||
desc: 'test description',
|
||||
layoutType: ViewLayoutPB.Board,
|
||||
});
|
||||
if (result.ok) {
|
||||
rootViews.push(result.val);
|
||||
}
|
||||
}
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const result = await workspaceService.createView({
|
||||
name: `test board 1 ${i}`,
|
||||
desc: 'test description',
|
||||
layoutType: ViewLayoutPB.Board,
|
||||
parentViewId: rootViews[0].id,
|
||||
});
|
||||
}
|
||||
|
||||
const allApps = await workspaceService.getAllViews();
|
||||
console.log(allApps);
|
||||
};
|
||||
|
||||
export const TestCreateViews = () => {
|
||||
const currentUser = useAppSelector((state) => state.currentUser);
|
||||
|
||||
return TestButton('Test create views', testCreateFolder, currentUser.id);
|
||||
};
|
||||
|
||||
const TestButton = (title: string, onClick: (userId?: number) => void, userId?: number) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div>
|
||||
<button className='rounded-md bg-pink-200 p-4' type='button' onClick={() => onClick(userId)}>
|
||||
{title}
|
||||
</button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
|
||||
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
|
||||
import { None, Some } from 'ts-results';
|
||||
import { RowBackendService } from '$app/stores/effects/database/row/row_bd_svc';
|
||||
import {
|
||||
makeDateTypeOptionContext,
|
||||
makeNumberTypeOptionContext,
|
||||
@@ -250,7 +249,7 @@ async function testCreateRow() {
|
||||
await databaseController.open().then((result) => result.unwrap());
|
||||
await assertNumberOfRows(view.id, 3);
|
||||
|
||||
// Create a row from a DatabaseController or create using the RowBackendService
|
||||
// Create a row from a DatabaseController
|
||||
await databaseController.createRow();
|
||||
await assertNumberOfRows(view.id, 4);
|
||||
await databaseController.dispose();
|
||||
@@ -262,8 +261,7 @@ async function testDeleteRow() {
|
||||
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 databaseController.deleteRow(rows[0].row.id);
|
||||
await assertNumberOfRows(view.id, 2);
|
||||
|
||||
// Wait the databaseViewCache get the change notification and
|
||||
|
||||
@@ -101,7 +101,7 @@ async function moveKanbanBoardRow() {
|
||||
});
|
||||
|
||||
const row = firstGroup.rowAtIndex(0).unwrap();
|
||||
await databaseController.moveRow(row.id, secondGroup.groupId);
|
||||
await databaseController.moveGroupRow(row.id, secondGroup.groupId);
|
||||
|
||||
assert(firstGroup.rows.length === 2);
|
||||
await assertNumberOfRowsInGroup(view.id, firstGroup.groupId, 2);
|
||||
|
||||
+17
-17
@@ -3,9 +3,9 @@ import { CellCache, CellCacheKey } from './cell_cache';
|
||||
import { CellDataLoader } from './data_parser';
|
||||
import { CellDataPersistence } from './data_persistence';
|
||||
import { FieldBackendService, TypeOptionParser } from '../field/field_bd_svc';
|
||||
import { ChangeNotifier } from '../../../../utils/change_notifier';
|
||||
import { ChangeNotifier } from '$app/utils/change_notifier';
|
||||
import { CellObserver } from './cell_observer';
|
||||
import { Log } from '../../../../utils/log';
|
||||
import { Log } from '$app/utils/log';
|
||||
import { Err, None, Ok, Option, Some } from 'ts-results';
|
||||
import { DatabaseFieldObserver } from '../field/field_observer';
|
||||
|
||||
@@ -48,14 +48,14 @@ export class CellController<T, D> {
|
||||
|
||||
/// 2.Listen on the field event and load the cell data if needed.
|
||||
void this.fieldNotifier.subscribe({
|
||||
onFieldChanged: () => {
|
||||
this.subscribeCallbacks?.onFieldChanged?.();
|
||||
onFieldChanged: async () => {
|
||||
/// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
|
||||
/// For example:
|
||||
/// ¥12 -> $12
|
||||
if (this.cellDataLoader.reloadOnFieldChanged) {
|
||||
void this._loadCellData();
|
||||
await this._loadCellData();
|
||||
}
|
||||
this.subscribeCallbacks?.onFieldChanged?.();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -97,24 +97,24 @@ export class CellController<T, D> {
|
||||
return cellData;
|
||||
};
|
||||
|
||||
private _loadCellData = () => {
|
||||
return this.cellDataLoader.loadData().then((result) => {
|
||||
if (result.ok) {
|
||||
const cellData = result.val;
|
||||
if (cellData.some) {
|
||||
this.cellCache.insert(this.cacheKey, cellData.val);
|
||||
this.cellDataNotifier.cellData = cellData;
|
||||
}
|
||||
} else {
|
||||
this.cellCache.remove(this.cacheKey);
|
||||
this.cellDataNotifier.cellData = None;
|
||||
private _loadCellData = async () => {
|
||||
const result = await this.cellDataLoader.loadData();
|
||||
if (result.ok) {
|
||||
const cellData = result.val;
|
||||
if (cellData.some) {
|
||||
this.cellCache.insert(this.cacheKey, cellData.val);
|
||||
this.cellDataNotifier.cellData = cellData;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.cellCache.remove(this.cacheKey);
|
||||
this.cellDataNotifier.cellData = None;
|
||||
}
|
||||
};
|
||||
|
||||
dispose = async () => {
|
||||
await this.cellObserver.unsubscribe();
|
||||
await this.fieldNotifier.unsubscribe();
|
||||
this.cellDataNotifier.unsubscribe();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
DatabaseEventCreateRow,
|
||||
DatabaseEventDeleteRow,
|
||||
DatabaseEventDuplicateRow,
|
||||
DatabaseEventGetDatabase,
|
||||
DatabaseEventGetDatabaseSetting,
|
||||
DatabaseEventGetFields,
|
||||
@@ -14,6 +16,7 @@ import {
|
||||
MoveGroupPayloadPB,
|
||||
MoveGroupRowPayloadPB,
|
||||
MoveRowPayloadPB,
|
||||
RowIdPB,
|
||||
} from '@/services/backend/events/flowy-database2';
|
||||
import {
|
||||
GetFieldPayloadPB,
|
||||
@@ -64,6 +67,16 @@ export class DatabaseBackendService {
|
||||
return DatabaseEventCreateRow(payload);
|
||||
};
|
||||
|
||||
duplicateRow = async (rowId: string) => {
|
||||
const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId });
|
||||
return DatabaseEventDuplicateRow(payload);
|
||||
};
|
||||
|
||||
deleteRow = async (rowId: string) => {
|
||||
const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId });
|
||||
return DatabaseEventDeleteRow(payload);
|
||||
};
|
||||
|
||||
/// Move the row from one group to another group
|
||||
/// [toRowId] is used to locate the moving row location.
|
||||
moveGroupRow = (fromRowId: string, toGroupId: string, toRowId?: string) => {
|
||||
|
||||
+10
-2
@@ -89,11 +89,19 @@ export class DatabaseController {
|
||||
return this.backendService.createRow();
|
||||
};
|
||||
|
||||
moveRow = (rowId: string, groupId: string) => {
|
||||
duplicateRow = async (rowId: string) => {
|
||||
return this.backendService.duplicateRow(rowId);
|
||||
};
|
||||
|
||||
deleteRow = async (rowId: string) => {
|
||||
return this.backendService.deleteRow(rowId);
|
||||
};
|
||||
|
||||
moveGroupRow = (rowId: string, groupId: string) => {
|
||||
return this.backendService.moveGroupRow(rowId, groupId);
|
||||
};
|
||||
|
||||
exchangeRow = async (fromRowId: string, toGroupId: string, toRowId?: string) => {
|
||||
exchangeGroupRow = async (fromRowId: string, toGroupId: string, toRowId?: string) => {
|
||||
await this.backendService.moveGroupRow(fromRowId, toGroupId, toRowId);
|
||||
await this.loadGroup();
|
||||
};
|
||||
|
||||
+7
-8
@@ -1,8 +1,8 @@
|
||||
import { Log } from "$app/utils/log";
|
||||
import { DatabaseBackendService } from "../database_bd_svc";
|
||||
import { DatabaseFieldChangesetObserver } from "./field_observer";
|
||||
import { FieldIdPB, FieldPB, IndexFieldPB } from "@/services/backend";
|
||||
import { ChangeNotifier } from "$app/utils/change_notifier";
|
||||
import { Log } from '$app/utils/log';
|
||||
import { DatabaseBackendService } from '../database_bd_svc';
|
||||
import { DatabaseFieldChangesetObserver } from './field_observer';
|
||||
import { FieldIdPB, FieldPB, IndexFieldPB } from '@/services/backend';
|
||||
import { ChangeNotifier } from '$app/utils/change_notifier';
|
||||
|
||||
export class FieldController {
|
||||
private backendService: DatabaseBackendService;
|
||||
@@ -53,7 +53,7 @@ export class FieldController {
|
||||
} else {
|
||||
Log.error(result.val);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -122,6 +122,5 @@ class NumOfFieldsNotifier extends ChangeNotifier<FieldInfo[]> {
|
||||
}
|
||||
|
||||
export class FieldInfo {
|
||||
constructor(public readonly field: FieldPB) {
|
||||
}
|
||||
constructor(public readonly field: FieldPB) {}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { CreateRowPayloadPB, RowIdPB } from '@/services/backend';
|
||||
import {
|
||||
DatabaseEventCreateRow,
|
||||
DatabaseEventDeleteRow,
|
||||
DatabaseEventDuplicateRow,
|
||||
DatabaseEventGetRow,
|
||||
} from '@/services/backend/events/flowy-database2';
|
||||
|
||||
export class RowBackendService {
|
||||
constructor(public readonly viewId: string) {}
|
||||
|
||||
// Create a row below the row with rowId
|
||||
createRow = (rowId: string) => {
|
||||
const payload = CreateRowPayloadPB.fromObject({ view_id: this.viewId, start_row_id: rowId });
|
||||
return DatabaseEventCreateRow(payload);
|
||||
};
|
||||
|
||||
deleteRow = (rowId: string) => {
|
||||
const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId });
|
||||
return DatabaseEventDeleteRow(payload);
|
||||
};
|
||||
|
||||
duplicateRow = (rowId: string) => {
|
||||
const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId });
|
||||
return DatabaseEventDuplicateRow(payload);
|
||||
};
|
||||
|
||||
getRow = (rowId: string) => {
|
||||
const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId });
|
||||
return DatabaseEventGetRow(payload);
|
||||
};
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import {
|
||||
FolderEventCreateView,
|
||||
FolderEventDeleteView,
|
||||
FolderEventMoveView,
|
||||
FolderEventReadView,
|
||||
FolderEventUpdateView,
|
||||
ViewLayoutPB,
|
||||
} from '@/services/backend/events/flowy-folder2';
|
||||
import {
|
||||
CreateViewPayloadPB,
|
||||
RepeatedViewIdPB,
|
||||
ViewPB,
|
||||
MoveViewPayloadPB,
|
||||
FlowyError,
|
||||
ViewIdPB,
|
||||
UpdateViewPayloadPB,
|
||||
} from '@/services/backend';
|
||||
import { None, Result, Some } from 'ts-results';
|
||||
|
||||
export class AppBackendService {
|
||||
constructor(public readonly appId: string) {}
|
||||
|
||||
getApp = () => {
|
||||
const payload = ViewIdPB.fromObject({ value: this.appId });
|
||||
return FolderEventReadView(payload);
|
||||
};
|
||||
|
||||
createView = async (params: {
|
||||
name: string;
|
||||
desc?: string;
|
||||
layoutType: ViewLayoutPB;
|
||||
/// The initial data should be the JSON of the document
|
||||
/// For example: {"document":{"type":"editor","children":[]}}
|
||||
initialData?: string;
|
||||
}) => {
|
||||
const encoder = new TextEncoder();
|
||||
const payload = CreateViewPayloadPB.fromObject({
|
||||
parent_view_id: this.appId,
|
||||
name: params.name,
|
||||
desc: params.desc || '',
|
||||
layout: params.layoutType,
|
||||
initial_data: encoder.encode(params.initialData || ''),
|
||||
});
|
||||
|
||||
const result = await FolderEventCreateView(payload);
|
||||
|
||||
if (result.ok) {
|
||||
return result.val;
|
||||
} else {
|
||||
throw new Error(result.val.msg);
|
||||
}
|
||||
};
|
||||
|
||||
getAllViews = (): Promise<Result<ViewPB[], FlowyError>> => {
|
||||
const payload = ViewIdPB.fromObject({ value: this.appId });
|
||||
return FolderEventReadView(payload).then((result) => {
|
||||
return result.map((app) => app.child_views);
|
||||
});
|
||||
};
|
||||
|
||||
getView = async (viewId: string) => {
|
||||
const result = await this.getAllViews();
|
||||
if (result.ok) {
|
||||
const target = result.val.find((view) => view.id === viewId);
|
||||
if (target !== undefined) {
|
||||
return Some(target);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
update = async (params: { name: string }) => {
|
||||
const payload = UpdateViewPayloadPB.fromObject({ view_id: this.appId, name: params.name });
|
||||
const result = await FolderEventUpdateView(payload);
|
||||
if (!result.ok) {
|
||||
throw new Error(result.val.msg);
|
||||
}
|
||||
};
|
||||
|
||||
delete = async () => {
|
||||
const payload = RepeatedViewIdPB.fromObject({ items: [this.appId] });
|
||||
const result = await FolderEventDeleteView(payload);
|
||||
if (!result.ok) {
|
||||
throw new Error(result.val.msg);
|
||||
}
|
||||
};
|
||||
|
||||
deleteView = (viewId: string) => {
|
||||
const payload = RepeatedViewIdPB.fromObject({ items: [viewId] });
|
||||
return FolderEventDeleteView(payload);
|
||||
};
|
||||
|
||||
moveView = (params: { view_id: string; fromIndex: number; toIndex: number }) => {
|
||||
const payload = MoveViewPayloadPB.fromObject({
|
||||
view_id: params.view_id,
|
||||
from: params.fromIndex,
|
||||
to: params.toIndex,
|
||||
});
|
||||
|
||||
return FolderEventMoveView(payload);
|
||||
};
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { FolderNotification } from '@/services/backend';
|
||||
import { ChangeNotifier } from '$app/utils/change_notifier';
|
||||
import { FolderNotificationObserver } from '../notifications/observer';
|
||||
|
||||
export class AppObserver {
|
||||
_viewsNotifier = new ChangeNotifier<void>();
|
||||
_listener?: FolderNotificationObserver;
|
||||
|
||||
constructor(public readonly appId: string) {}
|
||||
|
||||
subscribe = async (callbacks: { onViewsChanged: () => void }) => {
|
||||
this._viewsNotifier?.observer?.subscribe(callbacks.onViewsChanged);
|
||||
this._listener = new FolderNotificationObserver({
|
||||
viewId: this.appId,
|
||||
parserHandler: (notification, result) => {
|
||||
switch (notification) {
|
||||
case FolderNotification.DidUpdateChildViews:
|
||||
if (result.ok) {
|
||||
this._viewsNotifier?.notify();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
await this._listener.start();
|
||||
};
|
||||
|
||||
unsubscribe = async () => {
|
||||
this._viewsNotifier.unsubscribe();
|
||||
await this._listener?.stop();
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,25 @@
|
||||
import { UpdateViewPayloadPB, RepeatedViewIdPB, ViewPB } from '@/services/backend';
|
||||
import { UpdateViewPayloadPB, RepeatedViewIdPB, ViewPB, FlowyError, ViewIdPB } from '@/services/backend';
|
||||
import {
|
||||
FolderEventDeleteView,
|
||||
FolderEventDuplicateView,
|
||||
FolderEventReadView,
|
||||
FolderEventUpdateView,
|
||||
} from '@/services/backend/events/flowy-folder2';
|
||||
import { Ok, Result } from 'ts-results';
|
||||
|
||||
export class ViewBackendService {
|
||||
constructor(public readonly viewId: string) {}
|
||||
|
||||
getChildViews = async (): Promise<Result<ViewPB[], FlowyError>> => {
|
||||
const payload = ViewIdPB.fromObject({ value: this.viewId });
|
||||
const result = await FolderEventReadView(payload);
|
||||
if (result.ok) {
|
||||
return Ok(result.val.child_views);
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
update = (params: { name?: string; desc?: string }) => {
|
||||
const payload = UpdateViewPayloadPB.fromObject({ view_id: this.viewId });
|
||||
|
||||
@@ -26,7 +38,12 @@ export class ViewBackendService {
|
||||
return FolderEventDeleteView(payload);
|
||||
};
|
||||
|
||||
duplicate = (view: ViewPB) => {
|
||||
return FolderEventDuplicateView(view);
|
||||
duplicate = async () => {
|
||||
const view = await FolderEventReadView(ViewIdPB.fromObject({ value: this.viewId }));
|
||||
if (view.ok) {
|
||||
return FolderEventDuplicateView(view.val);
|
||||
} else {
|
||||
return view;
|
||||
}
|
||||
};
|
||||
}
|
||||
+23
-12
@@ -1,7 +1,7 @@
|
||||
import { Ok, Result } from "ts-results";
|
||||
import { DeletedViewPB, FolderNotification, ViewPB, FlowyError } from "@/services/backend";
|
||||
import { ChangeNotifier } from "$app/utils/change_notifier";
|
||||
import { FolderNotificationObserver } from "../notifications/observer";
|
||||
import { Ok, Result } from 'ts-results';
|
||||
import { DeletedViewPB, FolderNotification, ViewPB, FlowyError } from '@/services/backend';
|
||||
import { ChangeNotifier } from '$app/utils/change_notifier';
|
||||
import { FolderNotificationObserver } from '../notifications/observer';
|
||||
|
||||
type DeleteViewNotifyValue = Result<ViewPB, FlowyError>;
|
||||
type UpdateViewNotifyValue = Result<ViewPB, FlowyError>;
|
||||
@@ -12,17 +12,18 @@ export class ViewObserver {
|
||||
private _deleteViewNotifier = new ChangeNotifier<DeleteViewNotifyValue>();
|
||||
private _updateViewNotifier = new ChangeNotifier<UpdateViewNotifyValue>();
|
||||
private _restoreViewNotifier = new ChangeNotifier<RestoreViewNotifyValue>();
|
||||
private _moveToTashNotifier = new ChangeNotifier<MoveToTrashViewNotifyValue>();
|
||||
private _moveToTrashNotifier = new ChangeNotifier<MoveToTrashViewNotifyValue>();
|
||||
private _childViewsNotifier = new ChangeNotifier<void>();
|
||||
private _listener?: FolderNotificationObserver;
|
||||
|
||||
constructor(public readonly viewId: string) {
|
||||
}
|
||||
constructor(public readonly viewId: string) {}
|
||||
|
||||
subscribe = async (callbacks: {
|
||||
onViewUpdate?: (value: UpdateViewNotifyValue) => void;
|
||||
onViewDelete?: (value: DeleteViewNotifyValue) => void;
|
||||
onViewRestored?: (value: RestoreViewNotifyValue) => void;
|
||||
onViewMoveToTrash?: (value: MoveToTrashViewNotifyValue) => void;
|
||||
onChildViewsChanged?: () => void;
|
||||
}) => {
|
||||
if (callbacks.onViewDelete !== undefined) {
|
||||
this._deleteViewNotifier.observer?.subscribe(callbacks.onViewDelete);
|
||||
@@ -37,7 +38,11 @@ export class ViewObserver {
|
||||
}
|
||||
|
||||
if (callbacks.onViewMoveToTrash !== undefined) {
|
||||
this._moveToTashNotifier.observer?.subscribe(callbacks.onViewMoveToTrash);
|
||||
this._moveToTrashNotifier.observer?.subscribe(callbacks.onViewMoveToTrash);
|
||||
}
|
||||
|
||||
if (callbacks.onChildViewsChanged !== undefined) {
|
||||
this._childViewsNotifier.observer?.subscribe(callbacks.onChildViewsChanged);
|
||||
}
|
||||
|
||||
this._listener = new FolderNotificationObserver({
|
||||
@@ -67,15 +72,20 @@ export class ViewObserver {
|
||||
break;
|
||||
case FolderNotification.DidMoveViewToTrash:
|
||||
if (result.ok) {
|
||||
this._moveToTashNotifier.notify(Ok(DeletedViewPB.deserializeBinary(result.val)));
|
||||
this._moveToTrashNotifier.notify(Ok(DeletedViewPB.deserializeBinary(result.val)));
|
||||
} else {
|
||||
this._moveToTashNotifier.notify(result);
|
||||
this._moveToTrashNotifier.notify(result);
|
||||
}
|
||||
break;
|
||||
case FolderNotification.DidUpdateChildViews:
|
||||
if (result.ok) {
|
||||
this._childViewsNotifier?.notify();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
await this._listener.start();
|
||||
};
|
||||
@@ -84,7 +94,8 @@ export class ViewObserver {
|
||||
this._deleteViewNotifier.unsubscribe();
|
||||
this._updateViewNotifier.unsubscribe();
|
||||
this._restoreViewNotifier.unsubscribe();
|
||||
this._moveToTashNotifier.unsubscribe();
|
||||
this._moveToTrashNotifier.unsubscribe();
|
||||
this._childViewsNotifier.unsubscribe();
|
||||
await this._listener?.stop();
|
||||
};
|
||||
}
|
||||
+25
-14
@@ -1,9 +1,10 @@
|
||||
import { Err, Ok } from 'ts-results';
|
||||
import { Err, Ok, Result } from 'ts-results';
|
||||
import {
|
||||
FolderEventCreateView,
|
||||
FolderEventMoveView,
|
||||
FolderEventReadWorkspaceViews,
|
||||
FolderEventReadAllWorkspaces,
|
||||
ViewPB,
|
||||
} from '@/services/backend/events/flowy-folder2';
|
||||
import { CreateViewPayloadPB, FlowyError, MoveViewPayloadPB, ViewLayoutPB, WorkspaceIdPB } from '@/services/backend';
|
||||
import assert from 'assert';
|
||||
@@ -11,20 +12,25 @@ import assert from 'assert';
|
||||
export class WorkspaceBackendService {
|
||||
constructor(public readonly workspaceId: string) {}
|
||||
|
||||
createApp = async (params: { name: string; desc?: string }) => {
|
||||
createView = async (params: {
|
||||
name: string;
|
||||
desc?: string;
|
||||
layoutType: ViewLayoutPB;
|
||||
parentViewId?: string;
|
||||
/// The initial data should be the JSON of the document
|
||||
/// For example: {"document":{"type":"editor","children":[]}}
|
||||
initialData?: string;
|
||||
}) => {
|
||||
const encoder = new TextEncoder();
|
||||
const payload = CreateViewPayloadPB.fromObject({
|
||||
parent_view_id: this.workspaceId,
|
||||
parent_view_id: params.parentViewId ?? this.workspaceId,
|
||||
name: params.name,
|
||||
desc: params.desc || '',
|
||||
layout: ViewLayoutPB.Document,
|
||||
layout: params.layoutType,
|
||||
initial_data: encoder.encode(params.initialData || ''),
|
||||
});
|
||||
|
||||
const result = await FolderEventCreateView(payload);
|
||||
if (result.ok) {
|
||||
return result.val;
|
||||
} else {
|
||||
throw new Error(result.val.msg);
|
||||
}
|
||||
return FolderEventCreateView(payload);
|
||||
};
|
||||
|
||||
getWorkspace = () => {
|
||||
@@ -44,14 +50,19 @@ export class WorkspaceBackendService {
|
||||
});
|
||||
};
|
||||
|
||||
getApps = () => {
|
||||
getAllViews: () => Promise<Result<ViewPB[], FlowyError>> = async () => {
|
||||
const payload = WorkspaceIdPB.fromObject({ value: this.workspaceId });
|
||||
return FolderEventReadWorkspaceViews(payload).then((result) => result.map((val) => val.items));
|
||||
const result = await FolderEventReadWorkspaceViews(payload);
|
||||
if (result.ok) {
|
||||
return Ok(result.val.items);
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
moveApp = (params: { appId: string; fromIndex: number; toIndex: number }) => {
|
||||
moveView = (params: { viewId: string; fromIndex: number; toIndex: number }) => {
|
||||
const payload = MoveViewPayloadPB.fromObject({
|
||||
view_id: params.appId,
|
||||
view_id: params.viewId,
|
||||
from: params.fromIndex,
|
||||
to: params.toIndex,
|
||||
});
|
||||
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
import { FlowyError, FolderNotification } from '@/services/backend';
|
||||
import { NotificationParser, OnNotificationError } from '@/services/backend/notifications';
|
||||
import { Result } from 'ts-results';
|
||||
|
||||
declare type FolderNotificationCallback = (ty: FolderNotification, payload: Result<Uint8Array, FlowyError>) => void;
|
||||
|
||||
export class FolderNotificationParser extends NotificationParser<FolderNotification> {
|
||||
constructor(params: { id?: string; callback: FolderNotificationCallback; onError?: OnNotificationError }) {
|
||||
super(
|
||||
params.callback,
|
||||
(ty) => {
|
||||
const notification = FolderNotification[ty];
|
||||
if (isFolderNotification(notification)) {
|
||||
return FolderNotification[notification];
|
||||
} else {
|
||||
return FolderNotification.Unknown;
|
||||
}
|
||||
},
|
||||
params.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const isFolderNotification = (notification: string): notification is keyof typeof FolderNotification => {
|
||||
return Object.values(FolderNotification).indexOf(notification) !== -1;
|
||||
};
|
||||
@@ -5,7 +5,8 @@ export interface IPage {
|
||||
id: string;
|
||||
title: string;
|
||||
pageType: ViewLayoutPB;
|
||||
folderId: string;
|
||||
parentPageId: string;
|
||||
showPagesInside: boolean;
|
||||
}
|
||||
|
||||
const initialState: IPage[] = [];
|
||||
@@ -14,12 +15,19 @@ export const pagesSlice = createSlice({
|
||||
name: 'pages',
|
||||
initialState: initialState,
|
||||
reducers: {
|
||||
didReceivePages(state, action: PayloadAction<{ pages: IPage[]; folderId: string }>) {
|
||||
return state.filter((page) => page.folderId !== action.payload.folderId).concat(action.payload.pages);
|
||||
addInsidePages(state, action: PayloadAction<{ insidePages: IPage[]; currentPageId: string }>) {
|
||||
return state
|
||||
.filter((page) => page.parentPageId !== action.payload.currentPageId)
|
||||
.concat(action.payload.insidePages);
|
||||
},
|
||||
addPage(state, action: PayloadAction<IPage>) {
|
||||
state.push(action.payload);
|
||||
},
|
||||
toggleShowPages(state, action: PayloadAction<{ id: string }>) {
|
||||
return state.map<IPage>((page: IPage) =>
|
||||
page.id === action.payload.id ? { ...page, showPagesInside: !page.showPagesInside } : page
|
||||
);
|
||||
},
|
||||
renamePage(state, action: PayloadAction<{ id: string; newTitle: string }>) {
|
||||
return state.map<IPage>((page: IPage) =>
|
||||
page.id === action.payload.id ? { ...page, title: action.payload.newTitle } : page
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
ListenerEffectAPI,
|
||||
addListener,
|
||||
} from '@reduxjs/toolkit';
|
||||
import { foldersSlice } from './reducers/folders/slice';
|
||||
import { pagesSlice } from './reducers/pages/slice';
|
||||
import { navigationWidthSlice } from './reducers/navigation-width/slice';
|
||||
import { currentUserSlice } from './reducers/current-user/slice';
|
||||
@@ -25,7 +24,6 @@ const listenerMiddlewareInstance = createListenerMiddleware({
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
[foldersSlice.name]: foldersSlice.reducer,
|
||||
[pagesSlice.name]: pagesSlice.reducer,
|
||||
[activePageIdSlice.name]: activePageIdSlice.reducer,
|
||||
[navigationWidthSlice.name]: navigationWidthSlice.reducer,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Log } from '$app/utils/log';
|
||||
|
||||
export class AsyncQueue<T> {
|
||||
private queue: T[] = [];
|
||||
private isProcessing = false;
|
||||
private executeFunction: (item: T) => Promise<void>;
|
||||
|
||||
constructor(executeFunction: (item: T) => Promise<void>) {
|
||||
this.executeFunction = executeFunction;
|
||||
}
|
||||
|
||||
enqueue(item: T): void {
|
||||
this.queue.push(item);
|
||||
this.processQueue();
|
||||
}
|
||||
|
||||
private processQueue(): void {
|
||||
if (this.isProcessing || this.queue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = this.queue.shift();
|
||||
this.isProcessing = true;
|
||||
|
||||
const executeFn = async (item: T) => {
|
||||
try {
|
||||
await this.processItem(item);
|
||||
} catch (error) {
|
||||
Log.error('queue processing error:', error);
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
this.processQueue();
|
||||
}
|
||||
};
|
||||
|
||||
executeFn(item!);
|
||||
}
|
||||
|
||||
private async processItem(item: T): Promise<void> {
|
||||
try {
|
||||
await this.executeFunction(item);
|
||||
} catch (error) {
|
||||
Log.error('queue processing error:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ export abstract class AFNotificationObserver<T> {
|
||||
|
||||
async stop() {
|
||||
if (this._listener !== undefined) {
|
||||
// call the unlisten function before setting it to undefined
|
||||
this._listener();
|
||||
this._listener = undefined;
|
||||
}
|
||||
this.parser = null;
|
||||
|
||||
@@ -169,7 +169,7 @@ script = ["""
|
||||
cd appflowy_flutter/
|
||||
flutter clean
|
||||
flutter pub get
|
||||
flutter build ${TARGET_OS} --${BUILD_FLAG} --verbose
|
||||
flutter build ${TARGET_OS} --${BUILD_FLAG}
|
||||
"""]
|
||||
script_runner = "@shell"
|
||||
|
||||
@@ -188,6 +188,7 @@ script = [
|
||||
"""
|
||||
cd appflowy_flutter
|
||||
flutter clean
|
||||
flutter pub get
|
||||
flutter packages pub get
|
||||
dart run easy_localization:generate -S assets/translations/ -f keys -o locale_keys.g.dart -S assets/translations -s en.json
|
||||
dart run build_runner build -d
|
||||
@@ -200,6 +201,7 @@ script = [
|
||||
"""
|
||||
cd ./appflowy_flutter/
|
||||
exec cmd.exe /c flutter clean
|
||||
exec cmd.exe /c flutter pub get
|
||||
exec cmd.exe /c flutter packages pub get
|
||||
exec cmd.exe /c dart run easy_localization:generate -S assets/translations/ -f keys -o locale_keys.g.dart -S assets/translations -s en.json
|
||||
exec cmd.exe /c dart run build_runner build -d
|
||||
|
||||
@@ -33,7 +33,7 @@ dependencies = ["inner_build_test_backend"]
|
||||
description = "Run flutter unit tests"
|
||||
script = '''
|
||||
cd appflowy_flutter
|
||||
flutter test --dart-define=RUST_LOG=${RUST_LOG} -j, --concurrency=1 --coverage --verbose
|
||||
flutter test --dart-define=RUST_LOG=${RUST_LOG} -j, --concurrency=1 --coverage
|
||||
'''
|
||||
|
||||
[tasks.rust_unit_test]
|
||||
|
||||
Reference in new issue
Block a user