appflowy calendar view database
A look at the inner workings of the AppFlowy database through the development of a calendar view that enhances event scheduling and task management.
by Richard, as part of the AppFlowy Mentorship Program
Introduction
AppFlowy is powered by a robust database that stores large amounts of structured data. It is designed to help you input, store, and organize this data in an intuitive way.
The AppFlowy database can be thought of as simply a collection of rows and columns (or fields). Users can select a specific field type for a column that is most suited such as date, select options, or checklist. Some field types provide a popup that presents users with buttons and other controls to enter data, while other field types perform some data formatting and/or validation.
Additionally, each row can also store large amounts of text data. The rich-text editing area can be accessed by opening the row up as an overlay. This is really useful when the user needs to attach a report to the row, or simply add some text that is unique to each row.
There are additional tools that allow specific data to be highlighted, such as sorting and filtering.
In this post, we will explore another key concept of the database: database views. We will look at the motivation behind having multiple database views and how we created a new calendar view.
Through a calendar, users can grasp upcoming (or past) scheduled events at a glance. Calendars also allow users to easily identify the duration of an event, if and when events overlap, or how much time exists between two events. This feature would be especially useful for task scheduling and event planning.
The Case for Database Views
Let's consider an example of a database for books in a library. The database needs to keep track of all the information on the books that it offers to the visitors. This includes the title, author, publisher, date of publication, ISBN, borrow status, current borrower, date borrowed, latest date of return, etc.
The user can use the field type most suited for each kind of information (e.g., single select for borrow status, date and time for date borrowed).
However, we wish to provide users with a way to visualize the data in the database in different ways to give more useful insight. We can do this by allowing users to select which view layout to use to view the database.
Prior to this project, we only supported grid and Kanban board views. To give users the most flexibility, we separated data that should be customizable in each individual database view from the data that should be shared between all the database views. We will go into more detail about the structure of a database view in the following section.
Fundamentally, only the field and row information is shared between all the views (in our book example, this would be the books as rows and attributes of those books as fields). A database can have multiple database views at the same time. The user can switch between them using the tab bar above the database.
Continuing from our example, a Kanban board view that uses a “Status” field to group the rows could be added to provide an overview of the books that are in the library based on their availability — which ones are being borrowed, which ones are reserved, etc.
To go a step further, we can also add a calendar view and make it layout the rows according to the "Date Due" field. That way, users will be able to know when a book should be returned at a glance.
Let's now look at the structure of the database and the database view. It will illustrate the relationship between the two.
Behind the Scenes
First, let's consider the database. The first two properties shown below are related to data storage and synchronization. The subsequent properties are of greater interest to as, as they show the type of the information stored in the database.
fields
store information about the fields such as the name, id, and type option. metas
store the documents stored in each row, while block
stores all of the rows' data. And finally, views
store all the database views.1pub struct Database {2 inner: Arc<MutexCollab>,3 pub(crate) root: MapRefWrapper,4 pub fields: Rc<FieldMap>,5 pub metas: Rc<MetaMap>,6 pub block: Block,7 pub views: Rc<ViewMap>,8}
Contrast that with the structure of the
DatabaseView
and you will observe that lots configuration information are separated into from the Database
struct.Most of the fields in the struct are pretty straightforward. For example,
filters
and sorts
correspond to existing sort and filter configurations, while group_settings
corresponds to how rows may be grouped in a Kanban board, and field_orders
and row_orders
define how the rows and fields are ordered respectively.1pub struct DatabaseView {2 pub id: String,3 pub database_id: String,4 pub name: String,5 pub layout: DatabaseLayout,6 pub layout_settings: LayoutSettings,7 pub filters: Vec<FilterMap>,8 pub group_settings: Vec<GroupSettingMap>,9 pub sorts: Vec<SortMap>,10 pub row_orders: Vec<RowOrder>,11 pub field_orders: Vec<FieldOrder>,12 pub created_at: i64,13 pub modified_at: i64,14}
Layout Settings
We'll now look at a new configuration introduced for the calendar specifically that helps users to customize it:
layout_settings
. Through it, we will also see how other database views' properties are retrieved and stored.Layout settings are configurations of a database view that are specific to the view layout (grid, Kanban board or calendar).
These layout settings are primarily visual. Whether it is predicated on the current locale or simply a personal preference, users would be able to customize the look of their database views as much as they would like.
For the calendar view, we allow the following layout settings to be customized:
- The calendar layout type: while only month layout is supported currently, we are also hoping to have a week layout as well as a day layout in the future.
- First day of the week: in some parts of the world, Monday is the first day of the week while in others, Sunday is the first day of the week
- Show weekends: some people such as students or people working on weekdays only can choose to hide the weekends to save space
- Show week numbers: this is pretty self-explanatory
- field_id: this is the id of the date field being used to layout the events in the calendar. We’ll talk about this later.
Let's now see how this is implemented in code.
Our AppFlowy-Collab crate is responsible for data synchronization and storage throughout the entire application. In it, the
yrs
crate is used to store the data and support Conflict-free Replicated Data Type (CRDT).The database stores and processes data through a wrapper around a hash map of the
lib0
crate's Any
type, which can store any type of value and has efficient serialization for storage. We call this AnyMap
and we use it to store cell data, field type options, sort and filter configurations, etc.Of course, we also need to convert these to and from the Shared Types in
yrs
, including ArrayRef
and MapRef
.Below is a basic demonstration of the conversion between the
CalendarLayoutSetting
and AnyMap
type.1pub struct LayoutSettings(HashMap<DatabaseLayout, LayoutSetting>);2
3pub type LayoutSetting = AnyMap;4
5pub struct CalendarLayoutSetting {6 pub layout_ty: CalendarLayout,7 pub first_day_of_week: i32,8 pub show_weekends: bool,9 pub show_week_numbers: bool,10 pub field_id: String,11}12
13/// Converts an AnyMap to a CalendarLayoutSetting, used when reading from storage14/// or received from a synced device15impl From<LayoutSetting> for CalendarLayoutSetting {16 fn from(setting: LayoutSetting) -> Self {17 let layout_ty = setting18 .get_i64_value("layout_ty")19 .map(CalendarLayout::from)20 .unwrap_or_default();21 let first_day_of_week = setting22 .get_i64_value("first_day_of_week")23 .unwrap_or(DEFAULT_FIRST_DAY_OF_WEEK as i64) as i32;24 let show_weekends = setting25 .get_bool_value("show_weekends")26 .unwrap_or_default();27 let show_week_numbers = setting28 .get_bool_value("show_week_numbers")29 .unwrap_or_default();30 let field_id = setting.get_str_value("field_id").unwrap_or_default();31 Self {32 layout_ty,33 first_day_of_week,34 show_weekends,35 show_week_numbers,36 field_id,37 }38 }39}40
41/// Converts a CalendarLayoutSetting to an AnyMap to store and/or42/// sync to another device43impl From<CalendarLayoutSetting> for LayoutSetting {44 fn from(setting: CalendarLayoutSetting) -> Self {45 LayoutSettingBuilder::new()46 .insert_i64_value("layout_ty", setting.layout_ty.value())47 .insert_i64_value("first_day_of_week", setting.first_day_of_week as i64)48 .insert_bool_value("show_week_numbers", setting.show_week_numbers)49 .insert_bool_value("show_weekends", setting.show_weekends)50 .insert_str_value("field_id", setting.field_id)51 .build()52 }53}
Events and Notifications
AppFlowy provides a system of events and notifications implemented with Protobuf to communicate between the frontend and backend. (You can learn more about events and notifications in the AppFlowy Documentation)
While most of the events and notifications used in grids and Kanban boards are also usable in the calendar, we have created some additional implementations to pass event data more efficiently.
The first is events and notifications for layout settings, defined as
DatabaseEvent::GetLayoutSetting
and DatabaseEvent::SetLayoutSetting
.We also need to fetch the calendar events from the database. To illustrate how events are defined, below is an example definition of a calendar event:
1pub struct CalendarEventPB {2 #[pb(index = 1)]3 pub row_meta: RowMetaPB,4
5 #[pb(index = 2)]6 pub date_field_id: String,7
8 #[pb(index = 3)]9 pub title: String,10
11 #[pb(index = 4)]12 pub timestamp: i64,13
14 #[pb(index = 5)]15 pub is_scheduled: bool,16}
In grid and Kanban board, we can load the rows in a straightforward manner. In the calendar, however, we decided to also fetch a timestamp for the event based on the date field specified in the layout settings.
Doing this makes it much easier to write the frontend code, since the event information and the timestamp are now passed together. If we didn’t do this, we would have to get all of the rows in the database, then fetch the timestamp for each individual row.
Currently, there are four defined events:
GetAllCalendarEvents
GetNoDateCalendarEvents
GetCalendarEvent
MoveCalendarEvent
While the first 3 are simply getters that return one or more
CalendarEventPB
s, MoveCalendarEvent
is a special event that we use to re-schedule the event using drag and drop.After implementing the event handlers for each event, and we’re now ready to move to the frontend.
Building the Frontend
State Management Using Bloc
In the fronted, we take advantage of the
bloc
package for our state management needs.Thanks to the separation of logic from the Flutter widgets that it provides, we are able to focus on the processing of data in the Blocs. We then use the
flutter_bloc
package to create, manipulate and consume them in the widget tree.Let’s first look at the blocs used in other database views and see what we can re-use. There are a number of controllers and services in place to communicate with the backend. They are often initialized with the bloc managing the state of the database being presented to the user, but some others may be created as needed (such as a cell controller being created for a specific cell's editor bloc).
During initialization, listeners for notifications from the backend are also created.
DatabaseController
is a controller that encapsulates some specific controllers and services to provide specific functions. This is the main controller when a database is opened.DatabaseViewBackendService
can be used to make settings to the current database view (more on this later).FieldController
is used to edit the options of a field and manage filtering and sorting tasks on a field.DatabaseGroupListener
is used in database views that can group rows together based on a specific field, listening to group setting changes in the backend. This is currently only used in the Kanban board.DatabaseLayoutSettingListener
listens to layout setting changes. This is currently only used in Calendar.FilterBackendService
andSortBackendService
allow the user to create, modify and remove filtering and sorting configurations.- Various cell controllers used to listen to and allow making changes to cell data.
Since there are lots of overlap between different listeners, we’ll only look at the
DatabaseLayoutSettingListener
here.1class DatabaseLayoutSettingListener {2 final String viewId;3 PublishNotifier<LayoutSettingsValue<DatabaseLayoutSettingPB>>?4 _settingNotifier = PublishNotifier();5 DatabaseNotificationListener? _listener;6 DatabaseLayoutSettingListener(this.viewId);7
8 void start({9 required void Function(LayoutSettingsValue<DatabaseLayoutSettingPB>)10 onLayoutChanged,11 }) {12 _settingNotifier?.addPublishListener(onLayoutChanged);13 _listener = DatabaseNotificationListener(14 objectId: viewId,15 handler: _handler,16 );17 }18
19 void _handler(20 DatabaseNotification ty,21 Either<Uint8List, FlowyError> result,22 ) {23 switch (ty) {24 case DatabaseNotification.DidUpdateLayoutSettings:25 result.fold(26 (payload) => _settingNotifier?.value =27 left(DatabaseLayoutSettingPB.fromBuffer(payload)),28 (error) => _settingNotifier?.value = right(error),29 );30 break;31 default:32 break;33 }34 }35}
Upon starting, the listener listens for notifications coming from the backend.
When a
DidUpdateLayoutSettings
notification arrives, the _settingNotifier
is changed to the new value. This then triggers a series callback functions, ultimately ending up at the didReceiveCalendarSettings
event handler in the calendar bloc.1didReceiveCalendarSettings: (CalendarLayoutSettingPB settings) {2 // If the field id changed, reload all events3 state.settings.fold(() => null, (oldSetting) {4 if (oldSetting.fieldId != settings.fieldId) {5 _loadAllEvents();6 }7 });8 emit(state.copyWith(settings: Some(settings)));9}
As evident from the code snippet, we only need to load all the events (through the
GetAllCalendarEvents
that we have looked at earlier) if there is a change in the date field being used in the layout. This saves us from having to reload all the events when we only change the first day of week from Monday to Sunday, for example.Third-Party Calendar Component
With all the business logic set up and ready to go, the actual implementation of the calendar UI is pretty easy.
We use the calendar_view package to implement it, which provides a healthy level of customization and usability features.
This is our current design:
- A custom header area that shows the current month and year and allow users to navigate between months, change view settings and see the a list of unscheduled events in a drop-down menu.
- Event cards that not only show the title of the event, but also the other properties associated with it, such as select options.
- Users can click on the event card to open it as a page, where they can edit the event’s associated properties or hide, duplicate or delete the event.
- Users can also drag and drop the event cards to reschedule them.
- Users can switch to the previous or next month, and while in another month, jump to the current one.
We also plan to continuously update the UI to make it more beautiful, intuitive, and user-friendly.
Questionnaire
Thanks for reading this article. If you have some time, please kindly take our 1-minute survey to give us feedback and let us know what interests you the most.
If you have a suggestion or question for the calendar view or any part of AppFlowy, you can create an issue on GitHub and we’ll get back to you as soon as possible. Also feel free to follow us @appflowy on Twitter or join our Discord server to follow our latest development!