Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calendar link and sync #54

Merged
merged 16 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,14 @@ jobs:
fetch-depth: 1

- uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: 3.13.x

- name: Run pub get
run: |
cd flutterapp
flutter --version
flutter pub get

- name: Run flutter test
Expand Down
13 changes: 10 additions & 3 deletions assets/sass/components/flashMessage.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,26 @@
top: 16px;
}
}
.flash-messages {
display: flex;
flex-direction: column;
gap: $space;

.flash-message {
animation: 0.5s ease-out 0s 1 flash-message-append;
position: fixed;
left: 50%;
top: 16px;
width: max-content;
transform: translateX(-50%);
z-index: $z-flash;
}

.flash-message {
animation: 0.5s ease-out 0s 1 flash-message-append;

padding: $space * 2;
background: var(--color-bg);
box-shadow: var(--shadow-med);
border-radius: $border-radius;
z-index: $z-flash;

svg {
margin-right: $space;
Expand All @@ -34,6 +40,7 @@
&[data-state="hidden"] {
top: -60px;
opacity: 0;
display: none;
}
}

Expand Down
10 changes: 4 additions & 6 deletions assets/sass/pages/calendars/add.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
.calendar-add {
.calendar-name {
display: flex;
flex-direction: column;
gap: calc($space / 2);
}
.calendar-name {
display: flex;
align-items: center;
gap: calc($space / 2);
}
25 changes: 25 additions & 0 deletions config/Migrations/20240509041300_AddSyncedToCalendarSources.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);

use Migrations\AbstractMigration;

class AddSyncedToCalendarSources extends AbstractMigration
{
/**
* Change Method.
*
* More information on this method is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
* @return void
*/
public function change(): void
{
$table = $this->table('calendar_sources');
$table->addColumn('synced', 'boolean', [
// Before this all calendarsources were being synced.
'default' => true,
'null' => false,
]);
$table->update();
}
}
4 changes: 4 additions & 0 deletions config/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ function (RouteBuilder $builder) {
$builder->connect('/', ['action' => 'index'], ['_name' => 'calendarproviders:index']);
$builder->connect('/{id}/view', ['action' => 'view'], ['_name' => 'calendarproviders:view'])
->setPass(['id']);
$builder->post('/{id}/sync', ['action' => 'sync'], 'calendarproviders:sync')
->setPass(['id']);
$builder->connect('/{id}/delete', ['action' => 'delete'], ['_name' => 'calendarproviders:delete'])
->setPass(['id']);
});
Expand Down Expand Up @@ -280,6 +282,8 @@ function (RouteBuilder $builder) {
$builder->connect('/', ['action' => 'index'], ['_name' => 'calendarproviders:index']);
$builder->connect('/{id}/delete', ['action' => 'delete'], ['_name' => 'calendarproviders:delete'])
->setPass(['id']);
$builder->post('/{id}/sync', ['action' => 'sync'], 'calendarproviders:sync')
->setPass(['id']);
});

$builder->scope('/calendars/{providerId}/sources', ['controller' => 'CalendarSources'], function ($builder) {
Expand Down
2 changes: 1 addition & 1 deletion flutterapp/lib/actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,7 @@ Future<CalendarSource> createSource(String apiToken, CalendarSource source) asyn
/// Update the settings on a source.
Future<CalendarSource> updateSource(String apiToken, CalendarSource source) async {
var url = _makeUrl('/api/calendars/${source.calendarProviderId}/sources/${source.id}/edit');
var body = {'color': source.color, 'name': source.name};
var body = {'color': source.color, 'name': source.name, 'synced': source.synced};
var response = await httpPost(url, body: body, apiToken: apiToken, errorMessage: 'Could not update calendar settings');
return _decodeResponse(response.bodyBytes, (mapData) => CalendarSource.fromMap(mapData['source']));
}
Expand Down
8 changes: 6 additions & 2 deletions flutterapp/lib/models/calendarsource.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
int calendarProviderId;
String providerId;
int color;
bool synced;
DateTime? lastSync;

CalendarSource({
Expand All @@ -14,8 +15,9 @@
required this.calendarProviderId,
required this.providerId,
this.color = 0,
this.synced = true,
this.lastSync,
});
});

factory CalendarSource.fromMap(Map<String, dynamic> json) {
DateTime? lastSync;
Expand All @@ -30,13 +32,14 @@
calendarProviderId: json['calendar_provider_id'] ?? 0,
providerId: json['provider_id'] ?? '',
color: json['color'] ?? 0,
synced: json['synced'] ?? true,
lastSync: lastSync,
);
}

/// Linked sources are those with ids or providers.
get isLinked {
return id != 0 || calendarProviderId != 0;
return synced;

Check warning on line 42 in flutterapp/lib/models/calendarsource.dart

View check run for this annotation

Codecov / codecov/patch

flutterapp/lib/models/calendarsource.dart#L42

Added line #L42 was not covered by tests
}

Map<String, Object?> toMap() {
Expand All @@ -50,6 +53,7 @@
'provider_id': providerId,
'name': name,
'color': color,
'synced': synced,
'last_sync': syncStr,
};
}
Expand Down
20 changes: 17 additions & 3 deletions flutterapp/lib/screens/calendarproviderdetails.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class _CalendarProviderDetailsScreenState extends State<CalendarProviderDetailsS
}
}

enum Menu { link, sync, delete }
enum Menu { link, sync, delete, unlink }

class CalendarSourceItem extends StatelessWidget {
final CalendarSource source;
Expand All @@ -97,7 +97,7 @@ class CalendarSourceItem extends StatelessWidget {
}

late Widget leading;
if (source.isLinked) {
if (source.synced) {
leading = CalendarColourPicker(
key: const ValueKey("source-color"),
color: source.color,
Expand Down Expand Up @@ -138,6 +138,11 @@ class CalendarSourceItem extends StatelessWidget {
await viewmodel.linkSource(source);
messenger.showSnackBar(successSnackBar(context: context, text: "Calendar linked"));
},
Menu.unlink: () async {
var messenger = ScaffoldMessenger.of(context);
await viewmodel.unlinkSource(source);
messenger.showSnackBar(successSnackBar(context: context, text: "Calendar unlinked"));
},
Menu.sync: () async {
var messenger = ScaffoldMessenger.of(context);
await viewmodel.syncEvents(source);
Expand All @@ -157,7 +162,7 @@ class CalendarSourceItem extends StatelessWidget {
actions[item]?.call();
}, itemBuilder: (BuildContext context) {
List<PopupMenuEntry<Menu>> items = [];
if (source.isLinked) {
if (source.synced) {
items.add(
PopupMenuItem<Menu>(
value: Menu.sync,
Expand All @@ -167,6 +172,15 @@ class CalendarSourceItem extends StatelessWidget {
),
)
);
items.add(
PopupMenuItem<Menu>(
value: Menu.unlink,
child: ListTile(
leading: Icon(Icons.link_off, color: customColors.actionEdit),
title: const Text('Unlink'),
),
)
);
items.add(
PopupMenuItem<Menu>(
value: Menu.delete,
Expand Down
15 changes: 14 additions & 1 deletion flutterapp/lib/viewmodels/calendarproviderdetails.dart
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,27 @@ class CalendarProviderDetailsViewModel extends ChangeNotifier {
/// Link a calendar that will be synced
Future<void> linkSource(source) async {
source.calendarProviderId = provider.id;
source = await actions.createSource(_database.apiToken.token, source);
source.synced = true;
source = await actions.updateSource(_database.apiToken.token, source);

provider.replaceSource(source);
await _database.calendarDetails.set(provider);

notifyListeners();
}

/// Link a calendar that will be synced
Future<void> unlinkSource(source) async {
source.calendarProviderId = provider.id;
source.synced = false;
source = await actions.updateSource(_database.apiToken.token, source);

provider.removeSource(source);
await _database.calendarDetails.set(provider);

notifyListeners();
}

/// Update properties on a calendar source
Future<void> updateSource(source) async {
await actions.updateSource(_database.apiToken.token, source);
Expand Down
48 changes: 21 additions & 27 deletions flutterapp/test/screens/calendarproviderdetails_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'dart:convert';
import 'dart:io';
import 'package:docket/models/apitoken.dart';
import 'package:docket/models/calendarprovider.dart';
import 'package:docket/models/calendarsource.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart';
Expand All @@ -23,11 +22,6 @@ void main() {
final calendarResponse = file.readAsStringSync();
var decoded = jsonDecode(calendarResponse) as Map<String, dynamic>;
CalendarProvider provider = CalendarProvider.fromMap(decoded["provider"]);
provider.sources.add(CalendarSource.fromMap({
"name": "unlinked",
"provider_id": "fake-google.com",
"color": 1,
}));

group('$CalendarProviderDetailsScreen', () {
var db = LocalDatabase(inTest: true);
Expand Down Expand Up @@ -69,12 +63,12 @@ void main() {
);
});

testWidgets('sync action makes request', (tester) async {
testWidgets('delete action makes request', (tester) async {
var callCount = 0;
actions.client = MockClient((request) async {
if (request.url.path == '/api/calendars/5/sources/28/sync') {
if (request.url.path == '/api/calendars/5/sources/28/delete') {
callCount += 1;
return Response(calendarSourceResponse, 200);
return Response('', 200);
}
throw Exception('Request made to ${request.url.path} has no response');
});
Expand All @@ -83,25 +77,31 @@ void main() {
database: db,
child: CalendarProviderDetailsScreen(provider),
));
await tester.pumpAndSettle();
await tester.runAsync(() async {
await tester.pumpAndSettle();
});

// Open menu
var menu = find.byKey(const ValueKey('source-actions')).first;
await tester.tap(menu);
await tester.pumpAndSettle();

await tester.tap(find.text('Sync'));
await tester.tap(find.text('Delete'));
await tester.pumpAndSettle();

expect(find.text('Are you sure?'), findsOneWidget);
await tester.tap(find.text('Yes'));
await tester.pumpAndSettle();

expect(callCount, equals(1));
});

testWidgets('delete action makes request', (tester) async {
testWidgets('link action makes request', (tester) async {
var callCount = 0;
actions.client = MockClient((request) async {
if (request.url.path == '/api/calendars/5/sources/28/delete') {
if (request.url.path == '/api/calendars/5/sources/30/edit') {
callCount += 1;
return Response('', 200);
return Response(calendarSourceResponse, 200);
}
throw Exception('Request made to ${request.url.path} has no response');
});
Expand All @@ -110,29 +110,23 @@ void main() {
database: db,
child: CalendarProviderDetailsScreen(provider),
));
await tester.runAsync(() async {
await tester.pumpAndSettle();
});
await tester.pumpAndSettle();

// Open menu
var menu = find.byKey(const ValueKey('source-actions')).first;
var menu = find.byKey(const ValueKey('source-actions')).last;
await tester.tap(menu);
await tester.pumpAndSettle();

await tester.tap(find.text('Delete'));
await tester.pumpAndSettle();

expect(find.text('Are you sure?'), findsOneWidget);
await tester.tap(find.text('Yes'));
await tester.tap(find.text('Link'));
await tester.pumpAndSettle();

expect(callCount, equals(1));
});

testWidgets('link action makes request', (tester) async {
testWidgets('unlink action makes request', (tester) async {
var callCount = 0;
actions.client = MockClient((request) async {
if (request.url.path == '/api/calendars/5/sources/add') {
if (request.url.path == '/api/calendars/5/sources/28/edit') {
callCount += 1;
return Response(calendarSourceResponse, 200);
}
Expand All @@ -146,11 +140,11 @@ void main() {
await tester.pumpAndSettle();

// Open menu
var menu = find.byKey(const ValueKey('source-actions')).last;
var menu = find.byKey(const ValueKey('source-actions')).first;
await tester.tap(menu);
await tester.pumpAndSettle();

await tester.tap(find.text('Link'));
await tester.tap(find.text('Unlink').first);
await tester.pumpAndSettle();

expect(callCount, equals(1));
Expand Down
Loading