Commit 800d76ec authored by Azat Alimov's avatar Azat Alimov

Merge branch...

Merge branch '165-implement-authentication-flow-for-plugins-with-plugin-directing-cvu-to-display' into 'dev'

Resolve "Implement Authentication flow for plugins with plugin directing CVU to display"

Closes #165

See merge request !57
parents 8634d24e e53c1faf
Pipeline #3134 passed with stages
in 3 minutes and 5 seconds
.auth-view {
title: "Login"
defaultRenderer: generalEditor
showContextualBottomBar: false
showBottomBar: false
showDefaultLayout: false
[renderer = generalEditor] {
layout: [
{ section: username, fields: identifier, exclude: labels }
{ section: password, fields: secret, exclude: labels }
{ section: oAuthCode, fields: code, exclude: labels }
{ section: login }
]
}
login {
showTitle: false
HStack {
alignment: center
Spacer
Button {
padding: 20 0 20 0
onPress: [sync back]
VStack {
background: #218721
cornerRadius: 5
Text {
text: "Login"
font: 16 semibold
color: #fff
padding: 5 8 5 8
}
}
}
Spacer
}
}
}
......@@ -45,6 +45,21 @@
"property": "service",
"value_type": "string"
},
{
"item_type": "Account",
"property": "identifier",
"value_type": "string"
},
{
"item_type": "Account",
"property": "secret",
"value_type": "string"
},
{
"item_type": "Account",
"property": "code",
"value_type": "string"
},
{
"item_type": "Account",
"property": "itemType",
......@@ -1675,6 +1690,16 @@
"property": "task",
"value_type": "string"
},
{
"item_type": "StartPlugin",
"property": "state",
"value_type": "string"
},
{
"item_type": "StartPlugin",
"property": "oAuthUrl",
"value_type": "string"
},
{
"item_type": "StartPlugin",
"property": "container",
......
......@@ -21,6 +21,7 @@ import 'package:memri/MemriApp/Controllers/Database/ItemPropertyRecord.dart';
import 'package:memri/MemriApp/Controllers/Database/ItemRecord.dart';
import 'package:memri/MemriApp/Controllers/Database/PropertyDatabaseValue.dart';
import 'package:memri/MemriApp/Controllers/Database/Schema.dart';
import 'package:memri/MemriApp/Controllers/Plugins/PluginHandler.dart';
import 'package:memri/MemriApp/Controllers/SceneController.dart';
import 'package:memri/MemriApp/UI/ViewContext.dart';
import 'package:memri/MemriApp/UI/ViewContextController.dart';
......@@ -100,6 +101,8 @@ CVUAction Function({Map<String, CVUValue>? vars})? cvuAction(String named) {
return ({Map? vars}) => CVUActionMultiAction(vars: vars);
case "runindexer":
return ({Map? vars}) => CVUActionRunIndexer(vars: vars);
case "startplugin":
return ({Map? vars}) => CVUActionStartPlugin(vars: vars);
case "setproperty":
return ({Map? vars}) => CVUActionSetProperty(vars: vars);
case "setsetting":
......@@ -114,14 +117,14 @@ CVUAction Function({Map<String, CVUValue>? vars})? cvuAction(String named) {
return ({Map? vars}) => CVUActionToggleFullScreen(vars: vars);
case "selectall":
return ({Map? vars}) => CVUActionSelectAll(vars: vars);
case "sync":
return ({Map? vars}) => CVUActionSync(vars: vars);
case "deselectall":
return ({Map? vars}) => CVUActionDeselectAll(vars: vars);
case "tonextitem":
return ({Map? vars}) => CVUActionToNextItem(vars: vars);
case "topreviousitem":
return ({Map? vars}) => CVUActionToPreviousItem(vars: vars);
case "startplugin":
return ({Map? vars}) => CVUActionStartPlugin(vars: vars);
case "requestcontacts":
return ({Map? vars}) => CVUActionRequestContactsPermission(vars: vars);
case "requestlocation":
......@@ -451,9 +454,10 @@ class CVUActionStartPlugin extends CVUAction {
var lookup = CVULookupController();
var db = sceneController.appController.databaseController;
var plugin = context.currentItem;
var targetItemIdValue = vars["targetItemId"];
var containerValue = vars["container"];
if (targetItemIdValue == null || containerValue == null) return;
if (plugin == null || targetItemIdValue == null || containerValue == null) return;
String? targetItemId =
await lookup.resolve<String>(value: targetItemIdValue, context: context, db: db);
......@@ -464,17 +468,78 @@ class CVUActionStartPlugin extends CVUAction {
try {
var startPluginItem = ItemRecord(type: "StartPlugin");
await startPluginItem.save();
await startPluginItem.setPropertyValue("container", PropertyDatabaseValueString(container));
await startPluginItem.setPropertyValue(
"targetItemId", PropertyDatabaseValueString(targetItemId));
await startPluginItem.setPropertyValue("container", PropertyDatabaseValueString(container));
await startPluginItem.setPropertyValue("state", PropertyDatabaseValueString("idle"));
await AppController.shared.syncController.sync();
await PluginHandler.start(
plugin: plugin,
runner: startPluginItem,
sceneController: sceneController,
context: context);
} catch (error) {
print("Error starting plugin: $error");
}
}
}
class CVUActionSync extends CVUAction {
Map<String, CVUValue> vars;
CVUActionSync({vars}) : this.vars = vars ?? {};
@override
void execute(SceneController sceneController, CVUContext context) async {
try {
var pendingItems = <ItemRecord>[];
var pendingEdges = <ItemEdgeRecord>[];
var item = context.currentItem;
if (item != null) {
if (item.syncState == SyncState.skip) {
item.syncState = SyncState.create;
pendingItems.add(item);
}
var edges = await item.edges(null) + await item.reverseEdges(null);
for (var edge in edges) {
if (edge.syncState == SyncState.skip) {
edge.syncState = SyncState.create;
pendingEdges.add(edge);
}
}
var edgeItems = await item.edgeItems(null) + await item.reverseEdgeItems(null);
for (var edgeItem in edgeItems) {
if (edgeItem.syncState == SyncState.skip) {
edgeItem.syncState = SyncState.create;
pendingItems.add(edgeItem);
}
}
}
for (var item in pendingItems) {
await item.save();
}
for (var edge in pendingEdges) {
await edge.save();
}
await AppController.shared.syncController.sync();
if (sceneController.isInEditMode.value) {
sceneController.toggleEditMode();
}
} catch (error) {
print("Error starting sync: $error");
}
}
}
class CVUActionToggleEditMode extends CVUAction {
Map<String, CVUValue> vars;
......
......@@ -17,6 +17,7 @@ import 'package:json_annotation/json_annotation.dart';
part 'ItemRecord.g.dart';
enum SyncState {
skip,
create,
update,
noChanges,
......@@ -160,7 +161,9 @@ class ItemRecord with EquatableMixin {
ItemPropertyRecord(itemUID: uid, itemRowID: rowId!, name: name, value: value);
await itemPropertyRecord.save(db.databasePool);
}
syncState = state;
if (syncState != SyncState.skip) {
syncState = state;
}
/// Save the item record including the above changes - do this before editing the property so we know the item definitely exists
await save(db.databasePool);
......@@ -380,8 +383,7 @@ class ItemRecord with EquatableMixin {
static String? reverseMapSchemaValueType(String propertyValue) {
return _mapSchemaValueType.keys.firstWhere(
(nativeType) =>
_mapSchemaValueType[nativeType]?.toLowerCase() == propertyValue.toLowerCase(),
(nativeType) => _mapSchemaValueType[nativeType] == propertyValue,
orElse: () => propertyValue);
}
......@@ -420,10 +422,6 @@ class ItemRecord with EquatableMixin {
case "edgeName":
propertyValue = reverseMapSchemaPropertyName(propertyValue);
break;
case "sourceType":
case "targetType":
propertyValue = reverseMapSchemaValueType(propertyValue);
break;
}
break;
}
......
import 'package:memri/MemriApp/CVU/actions/CVUAction.dart';
import 'package:memri/MemriApp/CVU/definitions/CVUParsedDefinition.dart';
import 'package:memri/MemriApp/CVU/definitions/CVUValue.dart';
import 'package:memri/MemriApp/CVU/definitions/CVUValue_Constant.dart';
import 'package:memri/MemriApp/CVU/resolving/CVUContext.dart';
import 'package:memri/MemriApp/Controllers/AppController.dart';
import 'package:memri/MemriApp/Controllers/Database/ItemEdgeRecord.dart';
import 'package:memri/MemriApp/Controllers/Database/ItemRecord.dart';
import 'package:memri/MemriApp/Controllers/Database/PropertyDatabaseValue.dart';
import '../SceneController.dart';
class PluginHandler {
static start(
{required ItemRecord plugin,
required ItemRecord runner,
required SceneController sceneController,
required CVUContext context}) async {
AppController.shared.pubsubController.startObservingItemProperty(
item: runner,
property: "state",
desiredValue: null,
completion: (newValue, [error]) async {
if (newValue is PropertyDatabaseValueString) {
var state = newValue.value;
switch (state) {
case "userActionNeeded":
presentCVUforPlugin(
plugin: plugin,
runner: runner,
sceneController: sceneController,
context: context);
break;
default:
break;
}
return;
}
});
await AppController.shared.syncController.sync();
}
static presentCVUforPlugin(
{required ItemRecord plugin,
required ItemRecord runner,
required SceneController sceneController,
required CVUContext context}) async {
var runnerRowId = runner.rowId;
var view = await plugin.edgeItem("view");
var viewName = (await view?.propertyValue("definition"))?.value;
if (runnerRowId == null || viewName == null) {
AppController.shared.pubsubController
.stopObservingItemProperty(item: runner, property: "state");
return;
}
var item = ItemRecord(type: "Account");
item.syncState = SyncState.skip; // Don't sync it yet
await item.save();
var itemRowId = item.rowId;
if (itemRowId == null) {
return;
}
var edge = ItemEdgeRecord(sourceRowID: runnerRowId, name: "account", targetRowID: itemRowId);
edge.syncState = SyncState.skip; // Don't sync it yet
await edge.save();
var newVars = <String, CVUValue>{};
newVars["viewArguments"] = CVUValueSubdefinition(
CVUDefinitionContent(properties: {"readOnly": CVUValueConstant(CVUConstantBool(false))}));
await CVUActionOpenView(viewName: viewName, renderer: "generalEditor")
.execute(sceneController, context.replacingItem(item));
sceneController.isInEditMode.value = true;
}
}
......@@ -13,8 +13,8 @@ class ItemSubscription with EquatableMixin {
ItemRecord item;
String property;
int retryCount;
PropertyDatabaseValue desiredValue;
Function(PropertyDatabaseValue?, [String?]) completion;
PropertyDatabaseValue? desiredValue;
void Function(PropertyDatabaseValue?, [String?]) completion;
StreamSubscription? streamSubscription;
ItemSubscription(
......@@ -41,8 +41,9 @@ class PubSubController {
startObservingItemProperty(
{required ItemRecord item,
required String property,
required PropertyDatabaseValue desiredValue,
required Function(PropertyDatabaseValue?, [String?]) completion}) {
required PropertyDatabaseValue? desiredValue,
required void Function(PropertyDatabaseValue?, [String?]) completion}) {
stopObservingItemProperty(item: item, property: property);
var subscription = ItemSubscription(
item: item, property: property, desiredValue: desiredValue, completion: completion);
_subscribers.add(subscription);
......@@ -58,12 +59,15 @@ class PubSubController {
}
ItemSubscription? _subscriptionForItem({required ItemRecord item, required String property}) {
return _subscribers.firstWhereOrNull(
(element) => element.item.uid == item.uid && element.property == property);
return _subscribers.firstWhereOrNull((element) => element.item.uid == item.uid);
}
_cancelSubscription({required ItemSubscription subscription, String? error}) {
subscription.completion(null, error ?? "Cancelled");
_removeSubscription(subscription);
}
_removeSubscription(subscription) {
_subscribers.remove(subscription);
if (subscription.streamSubscription != null) {
subscription.streamSubscription?.cancel();
......@@ -72,51 +76,59 @@ class PubSubController {
}
_checkSubscriptionFulfilled(
{required dynamic itemProperty, required ItemSubscription subscription}) {
var containsValue = _containsExpectedValue(
itemProperty: itemProperty,
itemType: subscription.item.type,
property: subscription.property,
expectedValue: subscription.desiredValue);
if (containsValue) {
subscription.completion(subscription.desiredValue);
stopObservingItemProperty(item: subscription.item, property: subscription.property);
{required List<dynamic> dicts, required ItemSubscription subscription}) async {
var dict = dicts.asMap()[0];
if (dict == null) {
return;
}
var newValue = _propertyValueFromDict(
dict: dict, type: subscription.item.type, property: subscription.property);
if (newValue == null) {
_cancelSubscription(subscription: subscription, error: "Property not found");
return;
}
subscription.completion(newValue, null);
// If we have expected value to look for, check if new value is same as that of expected value
var expectedValue = subscription.desiredValue;
if (expectedValue == newValue) {
_removeSubscription(subscription);
}
}
_startObserver(ItemSubscription subscription) {
var stream = databaseController.databasePool
.itemPropertyRecordsCustomSelectStream("name = ? AND value = ? AND item = ?", [
Variable(subscription.property),
Variable(subscription.desiredValue.value),
Variable(subscription.item.rowId)
]);
var query = "name = ? AND item = ?";
var binding = [Variable(subscription.property), Variable(subscription.item.rowId)];
if (subscription.desiredValue != null) {
query += " AND value = ?";
binding.add(Variable(subscription.desiredValue!.value));
}
var stream =
databaseController.databasePool.itemPropertyRecordsCustomSelectStream(query, binding);
var streamSubscription = stream.listen((List<dynamic> records) {
if (records.isNotEmpty) {
_checkSubscriptionFulfilled(itemProperty: records[0], subscription: subscription);
_checkSubscriptionFulfilled(dicts: records, subscription: subscription);
}
});
subscription.streamSubscription = streamSubscription;
_subscriptions.add(streamSubscription);
}
bool _containsExpectedValue(
{required dynamic itemProperty,
required String itemType,
required String property,
required PropertyDatabaseValue expectedValue}) {
var decodableValue = itemProperty.value;
PropertyDatabaseValue? _propertyValueFromDict(
{required String property, required String type, required dynamic dict}) {
var decodableValue = dict.value;
if (decodableValue == null) {
return false;
return null;
}
var schema = AppController.shared.databaseController.schema;
var expectedType = schema.expectedPropertyType(itemType, property);
var expectedType = schema.expectedPropertyType(type, property);
if (expectedType == null) {
return false;
return null;
}
var databaseValue = PropertyDatabaseValue.create(decodableValue, expectedType);
return databaseValue == expectedValue;
return PropertyDatabaseValue.create(decodableValue, expectedType);
}
}
......@@ -104,7 +104,8 @@ class Settings {
var settings = await db.itemPropertyRecordsCustomSelect(query);
StringDb? setting = settings.firstWhere(
(setting) => setting is StringDb && setting.name == "keystr" && setting.value == path);
(setting) => setting is StringDb && setting.name == "keystr" && setting.value == path,
orElse: () => null);
if (setting == null) return null;
var settingRowID = setting.item;
......
......@@ -104,10 +104,15 @@ class _GeneralEditorRendererViewState extends State<GeneralEditorRendererView> {
return [];
}
var viewLayout = widget.viewContext.cvuController
.viewDefinitionForItemRecord(itemRecord: currentItem)
?.definitions
.asMap()[0]
?.get("layout");
.viewDefinitionForItemRecord(itemRecord: currentItem)
?.definitions
.asMap()[0]
?.get("layout") ??
widget.viewContext.cvuController
.viewDefinitionFor(viewName: widget.viewContext.config.viewName ?? "")
?.definitions
.asMap()[0]
?.get("layout");
List<Map<String, CVUValue>>? viewDefs = [];
if (viewLayout is CVUValueArray) {
......@@ -128,9 +133,21 @@ class _GeneralEditorRendererViewState extends State<GeneralEditorRendererView> {
return [];
}
var showDefaultLayout = true;
var showDefaultLayoutValue =
widget.viewContext.config.viewDefinition.properties["showDefaultLayout"];
if (showDefaultLayoutValue is CVUValueConstant) {
if (showDefaultLayoutValue.value is CVUConstantBool) {
showDefaultLayout = (showDefaultLayoutValue.value as CVUConstantBool).value;
}
}
var mergedDefinitions = [];
mergedDefinitions.addAll(viewDefs);
mergedDefinitions.addAll(generalDefs);
if (showDefaultLayout) {
// Merge layout from default generalEditor cvu
mergedDefinitions.addAll(generalDefs);
}
var sections = [];
Map<String, Map<String, CVUValue>> sectionInfos = {};
......@@ -328,6 +345,15 @@ class _GeneralEditorSectionState extends State<GeneralEditorSection> {
?.getSubdefinition();
if (nodeDefinition != null) {
return nodeDefinition;
} else {
var viewName = widget.viewContext.config.viewName;
if (viewName != null) {
var nodeDefinition = widget.viewContext.cvuController
.viewDefinitionFor(viewName: viewName)
?.properties[widget.layout.id]
?.getSubdefinition();
return nodeDefinition;
}
}
}
return null;
......@@ -502,13 +528,33 @@ class DefaultGeneralEditorRow extends StatelessWidget {
return false;
}
@override
Widget build(BuildContext context) {
var propType = property.valueType;
CVUDefinitionContent? get _nodeDefinition {
var nodeDefinition = viewContext.cvuController
.rendererDefinitionForSelector(viewName: viewContext.config.rendererName)
.viewDefinitionFor(viewName: viewContext.config.rendererName)
?.properties[prop]
?.getSubdefinition();
if (nodeDefinition == null) {
nodeDefinition = viewContext.cvuController
.rendererDefinitionForSelector(viewName: viewContext.config.rendererName)
?.properties[prop]
?.getSubdefinition();
}
if (nodeDefinition == null) {
var viewName = viewContext.config.viewName;
if (viewName != null) {
nodeDefinition = viewContext.cvuController
.viewDefinitionFor(viewName: viewName)
?.properties[prop]
?.getSubdefinition();
}
}
return nodeDefinition;
}
@override
Widget build(BuildContext context) {
var propType = property.valueType;
var nodeDefinition = _nodeDefinition;
Widget currentWidget = defaultRow();
if (nodeDefinition == null) {
switch (propType) {
......
......@@ -21,7 +21,7 @@ void main() {
item: record,
property: "state",
desiredValue: PropertyDatabaseValueString("userActionNeeded"),
completion: (newValue, [error]) async {
completion: (newValue, [error]) {
if (newValue == null) {
return;
}
......@@ -45,15 +45,48 @@ void main() {
print("`started` passed");
await record.setPropertyValue("state", PropertyDatabaseValueString("userActionNeeded"));
print("`userActionNeeded` passed");
await record.setPropertyValue("state", PropertyDatabaseValueString("userActionNeeded"));
print("`userActionNeeded` passed");
await record.setPropertyValue("state", PropertyDatabaseValueString("ready"));
print("`ready` passed");
});
test('testPluginAuthenticationFlow2', () async {
var record = ItemRecord(type: "StartPlugin");
await record.save();
await record.setPropertyValue("state", PropertyDatabaseValueString("idle"));
appController.pubsubController.startObservingItemProperty(
item: record,
property: "state",
desiredValue: null,
completion: (newValue, [error]) async {
if (newValue is PropertyDatabaseValueString) {
var state = newValue.value;
print("got `$state`");
var recordNotd = ItemRecord(type: "Note");
await recordNotd.save();
await recordNotd.setPropertyValue("title", PropertyDatabaseValueString("by Ani"));
switch (state) {
case "userActionNeeded":
print("presentCVUforPlugin");
break;
default:
break;
}
return;
}
});
await record.setPropertyValue("state", PropertyDatabaseValueString("started"));
print("`started` passed");
await record.setPropertyValue("state", PropertyDatabaseValueString("userActionNeeded"));
print("`userActionNeeded` passed");
await record.setPropertyValue("state", PropertyDatabaseValueString("userActionNeeded"));
print("`userActionNeeded` passed");
await record.setPropertyValue("state", PropertyDatabaseValueString("ready"));
print("`ready` passed");
});
tearDown(() async {
tearDownAll(() async {
appController.databaseController.databasePool.close();
});
}