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

feat: add template #3032

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
99ecf59
feat: add template
AmanNegi Jul 20, 2023
646876a
feat: add integration tests
AmanNegi Jul 22, 2023
53803ab
chore: merge with main
AmanNegi Aug 8, 2023
2b929ad
fix: update integration test
AmanNegi Aug 8, 2023
6301d1b
feat: Fix tests and template button
AmanNegi Aug 11, 2023
f92734b
chore: merge with main
AmanNegi Aug 17, 2023
118f871
chore: fix errors
AmanNegi Aug 17, 2023
8d4cf4b
fix: analyzing issue
AmanNegi Aug 17, 2023
f9c069d
feat: Support nested pages
AmanNegi Aug 25, 2023
01deb81
chore: merge with main
AmanNegi Aug 25, 2023
0bb9290
chore: add tests to runner
AmanNegi Aug 29, 2023
b275039
chore: merge with main
AmanNegi Aug 30, 2023
d893f7a
feat: add image support
AmanNegi Sep 7, 2023
81a4b0f
chore: merge with main
AmanNegi Sep 7, 2023
5a92cda
chore: update i8n
AmanNegi Sep 8, 2023
2661fe4
chore: merge with main
AmanNegi Oct 5, 2023
ebd643d
Merge branch 'main' into feat-create-template
AmanNegi Oct 5, 2023
512b9d4
chore: merge with main
AmanNegi Oct 10, 2023
e9e2021
chore: merge with main
AmanNegi Oct 10, 2023
21ce591
chore: merge with 'main'
AmanNegi Oct 14, 2023
b6e1cb6
feat: export template as zip
AmanNegi Oct 14, 2023
f9c44a8
Merge branch 'main' of https://github.com/AmanNegi/AppFlowy into feat…
AmanNegi Oct 17, 2023
5625cf0
chore: merge with main
AmanNegi Dec 11, 2023
56abf2c
fix: minor bugs
AmanNegi Dec 11, 2023
b8494f8
chore: add initial templates
AmanNegi Dec 13, 2023
815e864
feat: add support for referenced grid
AmanNegi Dec 21, 2023
39b07fc
chore: Merge branch 'main' of https://github.com/AmanNegi/AppFlowy in…
AmanNegi Dec 21, 2023
441fbe9
feat: add template tests
AmanNegi Dec 22, 2023
8420071
chore: merge with main branch
AmanNegi Dec 22, 2023
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
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import 'package:flutter/services.dart';
AmanNegi marked this conversation as resolved.
Show resolved Hide resolved
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'dart:io';

import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';

import '../util/mock/mock_file_picker.dart';
import '../util/util.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

group('document template test', () {
testWidgets('export a template tree', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();

await tester.createNewPageWithName(
name: 'parentDoc',
layout: ViewLayoutPB.Document,
);

await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText("# Parent Doc");

await tester.createNewPageWithName(
name: "childDoc",
parentName: "parentDoc",
layout: ViewLayoutPB.Document,
);
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText("# Child Doc");

await tester.createNewPageWithName(
name: "childGrid",
parentName: "parentDoc",
layout: ViewLayoutPB.Grid,
);

await tester.openPage(gettingStarted);

await tester.editor.hoverOnCoverToolbar();
await tester.tapButtonWithName("Convert to JSON");
AmanNegi marked this conversation as resolved.
Show resolved Hide resolved

final tempDir = await getApplicationDocumentsDirectory();

expect(await Directory("${tempDir.path}/template").exists(), isTrue);

expect(
await File("${tempDir.path}/template/config.json").exists(),
isTrue,
);
expect(
await File("${tempDir.path}/template/parentDoc.json").exists(),
isTrue,
);
expect(
await File("${tempDir.path}/template/childDoc.json").exists(),
isTrue,
);
expect(
await File("${tempDir.path}/template/childGrid.csv").exists(),
isTrue,
);
});

testWidgets('import a template', (tester) async {
final context = await tester.initializeAppFlowy();
await tester.tapGoButton();

await tester.editor.tapLineOfEditorAt(0);

const zipFileName = 'template.zip';
final data = await rootBundle.load('assets/test/workspaces/$zipFileName');

final bytes = Uint8List.view(data.buffer);
final path = p.join(context.applicationDataDirectory, zipFileName);
File(path).writeAsBytesSync(bytes);

// mock get files
await mockPickFilePaths(paths: [path]);

// tap template button
await tester.tapAddViewButton();
await tester.tapButtonWithName("Template");

await tester.expandPage(gettingStarted);

await tester.pumpAndSettle();

// Expand all pages
final List<String> toExpand = [
"TestTemplate",
"Level1_1",
"Level1_2",
"Level2_1",
];

for (final e in toExpand) {
await tester.expandPage(e);
}

await tester.pumpAndSettle();

tester.expectToSeePageName("TestTemplate", parentName: gettingStarted);

tester.expectToSeePageName("Level1_1", parentName: "TestTemplate");
tester.expectToSeePageName("Level2_1", parentName: "Level1_1");
tester.expectToSeePageName("Level3_1", parentName: "Level2_1");

tester.expectToSeePageName("Level1_2", parentName: "TestTemplate");
tester.expectToSeePageName("Level2_2", parentName: "Level1_2");
tester.expectToSeeText("Level2_Grid");
});
});
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
Expand Down Expand Up @@ -409,6 +410,19 @@ extension CommonOperations on WidgetTester {
await gesture.up();
await pumpAndSettle();
}

Future<void> expandPage(String name) async {
final pageToggleButton = find.descendant(
of: findPageName(name),
matching: find.byWidgetPredicate(
// (widget) => widget is FlowySvg && widget.name == 'home/drop_down_hide',
AmanNegi marked this conversation as resolved.
Show resolved Hide resolved
(widget) =>
widget is FlowySvg && widget.svg == FlowySvgs.drop_menu_hide_m,
),
);
await tap(pageToggleButton);
await pumpAndSettle(const Duration(milliseconds: 500));
}
}

extension ViewLayoutPBTest on ViewLayoutPB {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import 'dart:convert';
import 'dart:io';

import 'package:path/path.dart' as path;
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:path_provider/path_provider.dart';

/// Responsible to generate the [config.json] file for template

class ConfigService {
final _template = FlowyTemplate(
templateName: "template",
documents: FlowyTemplateItem(name: "template.json", childViews: []),
);

initConfig(ViewPB view) async {
final docModel = await _generateConfig(view, _template.documents);
if (docModel != null) _template.documents = docModel;
}

Future<FlowyTemplateItem?> _generateConfig(
ViewPB view,
FlowyTemplateItem model,
) async {
final viewsAtId = await ViewBackendService.getChildViews(viewId: view.id);
final List<ViewPB> views = viewsAtId.getLeftOrNull();

if (views.isEmpty) return null;

final FlowyTemplateItem newModel =
FlowyTemplateItem(name: "${view.name}.json", childViews: []);

for (final e in views) {
final temp = await ViewBackendService.getChildViews(viewId: e.id);
final viewsAtE = temp.getLeftOrNull();

// If children are empty no need to continue
if (viewsAtE.isEmpty) {
newModel.childViews.add(addData(newModel, e));
} else {
final newDoc = await _generateConfig(e, newModel);

if (newDoc != null) {
newModel.childViews.add(newDoc);
}
}
}

return newModel;
}

Future<void> saveConfig() async {
final directory = await getApplicationDocumentsDirectory();

final dir = Directory(path.join(directory.path, 'template'));
if (!(await dir.exists())) {
await dir.create(recursive: true);
}

final file = File(
path.join(directory.path, 'template', "config.json"),
);

await file.writeAsString(json.encode(_template.toJson()));
}

FlowyTemplateItem addData(FlowyTemplateItem model, ViewPB view) {
final FlowyTemplateItem newModel = FlowyTemplateItem(
name: "",
childViews: [],
);

switch (view.layout) {
case ViewLayoutPB.Document:
newModel.name = "${view.name}.json";
break;
case ViewLayoutPB.Grid:
case ViewLayoutPB.Board:
final name = "${view.name}.csv";
newModel.name = name;
break;
default:
// Eventually support calender
}
return newModel;
}
}

/// [FlowyTemplate] is the structure for [config.json] file
class FlowyTemplate {
String templateName;
FlowyTemplateItem documents;

FlowyTemplate({
required this.templateName,
required this.documents,
});

factory FlowyTemplate.fromRawJson(String str) =>
FlowyTemplate.fromJson(json.decode(str));

String toRawJson() => json.encode(toJson());

factory FlowyTemplate.fromJson(Map<String, dynamic> json) => FlowyTemplate(
templateName: json["templateName"],
documents: FlowyTemplateItem.fromJson(json["documents"]),
);

Map<String, dynamic> toJson() => {
"templateName": templateName,
"documents": documents.toJson(),
};
}

/// [FlowyTemplateItem] is the structure for each document in [config.json] file
class FlowyTemplateItem {
String name;
// List<String> db;
List<FlowyTemplateItem> childViews;

FlowyTemplateItem({
required this.name,
// required this.db,
required this.childViews,
});

factory FlowyTemplateItem.fromRawJson(String str) =>
FlowyTemplateItem.fromJson(json.decode(str));

String toRawJson() => json.encode(toJson());

factory FlowyTemplateItem.fromJson(Map<String, dynamic> json) =>
FlowyTemplateItem(
name: json["name"],
childViews: List<FlowyTemplateItem>.from(
json["childViews"].map((x) => FlowyTemplateItem.fromJson(x)),
),
);

Map<String, dynamic> toJson() {
final processed = <FlowyTemplateItem>{};
final result = <String, dynamic>{
"name": name,
"childViews": [],
};

_toJsonHelper(this, processed, result);

return result;
}

void _toJsonHelper(
FlowyTemplateItem model,
Set<FlowyTemplateItem> processed,
Map<String, dynamic> result,
) {
if (processed.contains(model)) {
return;
}

processed.add(model);

final childViews = <dynamic>[];
for (final child in model.childViews) {
final childResult = <String, dynamic>{
"name": child.name,
"childViews": [],
};

_toJsonHelper(child, processed, childResult);
childViews.add(childResult);
}

result["childViews"] = childViews;
}
}
Loading