diff --git a/CHANGELOG.md b/CHANGELOG.md index f9acd777..25413d58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Visit the package at [pub.dev](https://pub.dev/packages/solidpod). ## 0.10 Further UI migrations ++ Move grant permission form to dialog [0.9.12 20260119 jesscmoore] + Standardise action message colors [0.9.11 20260116 jesscmoore] + Fixed sharing to public/auth users bug [0.9.10 20260115 jesscmoore] + Fixed error when reading large file as single chunk [0.9.9 20260117 cdawei] diff --git a/example/.metadata b/example/.metadata index 2e80cfec..644060a6 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2" + revision: "3b62efc2a3da49882f43c372e0bc53daef7295a6" channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 - base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 - - platform: windows - create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 - base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 + base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 + - platform: web + create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 + base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6 # User provided section diff --git a/lib/src/solid/constants/ui.dart b/lib/src/solid/constants/ui.dart index 5c4034c1..7e3e639c 100644 --- a/lib/src/solid/constants/ui.dart +++ b/lib/src/solid/constants/ui.dart @@ -258,3 +258,31 @@ class WebIdLayout { static const listPadding = EdgeInsets.fromLTRB(0, 5, 0, 5); } + +/// Layout constants used for Grant Permission Form Dialog + +class GrantPermFormLayout { + /// Vertical gap between paragraphs + + static const paraVertGap = SizedBox(height: 10); + + /// Standard padding for dialog content. + + static const contentPadding = EdgeInsets.symmetric(horizontal: 50); + + /// Standard width for security dialogs. + + static const dialogWidth = 480.0; + + /// Height of dropdown suggestion box. + + static const dropdownHeight = 120.0; + + /// Elevation of dropdown suggestion cards. + + static double dropdownElevation = 5; + + /// Padding of dropdown suggestion list. + + static const listPadding = EdgeInsets.fromLTRB(0, 5, 0, 5); +} diff --git a/lib/src/solid/grant_permission_form.dart b/lib/src/solid/grant_permission_form.dart new file mode 100644 index 00000000..2f476c44 --- /dev/null +++ b/lib/src/solid/grant_permission_form.dart @@ -0,0 +1,436 @@ +/// A button for sharing a resource. +/// +// Time-stamp: +/// +/// Copyright (C) 2024-2025, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Jess Moore, Anushka Vidanage + +library; + +import 'package:flutter/material.dart'; + +import 'package:solidpod/src/solid/constants/ui.dart'; +import 'package:solidpod/src/solid/constants/web_acl.dart'; +import 'package:solidpod/src/solid/grant_permission.dart'; +import 'package:solidpod/src/solid/grant_permission_helper.dart'; +import 'package:solidpod/src/solid/select_recipients.dart'; +import 'package:solidpod/src/solid/show_selected_recipients.dart'; +import 'package:solidpod/src/solid/solid_func_call_status.dart'; +import 'package:solidpod/src/solid/utils/alert.dart'; +import 'package:solidpod/src/solid/utils/is_phone.dart'; +import 'package:solidpod/src/solid/utils/snack_bar.dart'; +import 'package:solidpod/src/widgets/group_webid_input_dialog.dart'; +import 'package:solidpod/src/widgets/ind_webid_input_dialog.dart'; + +/// Sharing (grant permission) form dialog function +/// +/// A [StatefulWidget] for creating a grant permission form +/// dialog to get recipient and access modes to grant for the +/// provided [resourceName] +/// +/// Parameters: +/// - [resourceName] - The filename or file url of the resource. If [isExternalRes], it should be the url of the resource. +/// - [isExternalRes] - Boolean flag describing whether the resource +/// is externally owned. +/// - [ownerWebId] - WebId of the owner of the resource. Required if the resource is externally owned. +/// - [granterWebId] - WebId of the granter of the resource. Required if the resource is externally owned. +/// - [accessModeList] - List of access mode options to show. +/// - [recipientTypeList] - List of recipient type options to show. +/// - [isFile] - Boolean flag describing whether the resource is a file. If false, the resource is assumed to be a directory. +/// - [updatePermissionsFunction] is the function to be called to refresh the permission table. +/// - [updatePermissionGrantedFunction] - is the function to be called +/// when permissions are granted successfully +/// - [onPermissionGranted] - Callback function called when permissions are granted successfully. + +class GrantPermissionForm extends StatefulWidget { + /// String to assign the webId of the resource owner. + + final String ownerWebId; + + /// String to assign the external webId of the resource granter. + + final String granterWebId; + + /// The name of the file or directory that access is being granted for. + + final String resourceName; + + final bool isExternalRes; + + /// A flag to determine whether the given resource is a file or not. + + final bool isFile; + + /// The list of access modes to show in form. By default + /// all four types of access mode are listed. + + final List accessModeList; + + /// The list of types of recipients to show in form. By default + /// all four types of recipient are listed. + + final List recipientTypeList; + + /// Map of data files on a user's POD used to extract the + /// user's recipient list by the WebIdTextInputScreen. + /// If not provided, the WebIdTextInputScreen will read the + /// user's files in their app data folder on their Pod to + /// fetch the ACLs needed to derive the user's recipient list. + + final Map dataFilesMap; + + /// Function run to update permissions table + + final Function updatePermissionsFunction; + + /// Function when permissions are granted successfully + + final Function updatePermissionGrantedFunction; + + /// Callback function called when permissions are granted successfully. + + final VoidCallback? onPermissionGranted; + + const GrantPermissionForm({ + super.key, + required this.updatePermissionsFunction, + required this.resourceName, + required this.ownerWebId, + required this.granterWebId, + this.accessModeList = const ['read', 'write', 'append', 'control'], + this.recipientTypeList = const ['public', 'indi', 'auth', 'group'], + required this.isExternalRes, + required this.isFile, + required this.updatePermissionGrantedFunction, + this.dataFilesMap = const {}, + this.onPermissionGranted, + }); + + @override + State createState() => _GrantPermissionFormState(); +} + +class _GrantPermissionFormState extends State { + /// Selected recipient + + RecipientType selectedRecipientType = RecipientType.none; + + /// Selected recipient details + + String selectedRecipientDetails = ''; + + /// List of webIds for group permission + + List finalWebIdList = []; + + /// Selected list of permissions + + List selectedPermList = []; + + /// Group name text controller + + final groupNameController = TextEditingController(); + + /// Group of webIds text controller + + final groupWebIdsController = TextEditingController(); + + /// Flag to track if permissions were granted successfully. + + bool permissionsGrantedSuccessfully = false; + + /// read permission checked flag + + bool readChecked = false; + + /// write permission checked flag + + bool writeChecked = false; + + /// control permission checked flag + + bool controlChecked = false; + + /// append permission checked flag + + bool appendChecked = false; + + /// Public permission check flag + + bool publicChecked = false; + + /// Define access mode list + + List accessModeList = []; + + @override + void initState() { + super.initState(); + + // Load access mode list to be displayed + for (final accessModeStr in widget.accessModeList) { + accessModeList.add(getAccessMode(accessModeStr)); + } + } + + @override + void dispose() { + groupNameController.dispose(); // Dispose group name editing controller + groupWebIdsController.dispose(); // Dispose group webids editing controller + super.dispose(); + } + + /// Private function to call alert dialog in share resource button + /// context. This provides an alert dialog over the top of the + /// grant permission form dialog. + Future _alert(String msg) async => alert(context, msg); + + /// Private function to show snackbar in share resource button context + Future _showSnackBar( + String msg, + Color bgColor, { + Duration duration = const Duration(seconds: 4), + }) async => + showSnackBar(context, msg, bgColor, duration: duration); + + /// Update selected webid list with individual recipient webid + /// [receiverWebId]. + void updateIndWebIdInput( + String receiverWebId, + ) => + setState(() { + selectedRecipientType = RecipientType.individual; + selectedRecipientDetails = receiverWebId; + finalWebIdList = [receiverWebId]; + }); + + /// Update selected webid list with list of webids in + /// recipient group [webIdList] and their group name + /// [groupName]. + + void updateGroupWebIdInput( + String groupName, + List webIdList, + ) => + setState(() { + selectedRecipientType = RecipientType.group; + selectedRecipientDetails = + '$groupName with WebIDs ${webIdList.join(', ')}'; + finalWebIdList = webIdList; + }); + + /// Update checked status of access mode boxes to show + /// selected access modes. + void updateCheckbox(bool newValue, AccessMode accessMode) => setState(() { + switch (accessMode) { + case AccessMode.read: + readChecked = newValue; + case AccessMode.write: + writeChecked = newValue; + case AccessMode.control: + controlChecked = newValue; + case AccessMode.append: + appendChecked = newValue; + } + if (newValue) { + selectedPermList.add(accessMode.mode); + } else { + selectedPermList.remove(accessMode.mode); + } + }); + + /// Define button click actions for each recipient type button + + /// Set recipients to public + void _setRecipientsToPublic() => setState(() { + selectedRecipientType = RecipientType.public; + selectedRecipientDetails = ''; + finalWebIdList = [publicAgent.value]; + }); + + /// Set recipients to authorised users + void _setRecipientsToAuthUsers() => setState(() { + selectedRecipientType = RecipientType.authUser; + selectedRecipientDetails = ''; + finalWebIdList = [authenticatedAgent.value]; + }); + + /// Select individual recipient + void _setRecipientsToIndividual() async => await indWebIdInputDialog( + context, + updateIndWebIdInput, + widget.dataFilesMap, + ); + + /// Select a group of recipients + void _setRecipientsToGroup() async => await groupWebIdInputDialog( + context, + groupNameController, + groupWebIdsController, + updateGroupWebIdInput, + ); + + @override + Widget build(BuildContext context) { + return AlertDialog( + insetPadding: GrantPermFormLayout.contentPadding, + title: Text( + 'Share ${widget.resourceName}', + ), + content: SizedBox( + // Use full width on phones, else use a preset narrower width + width: + (!isPhone()) ? GrantPermFormLayout.dialogWidth : double.maxFinite, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + getHeading( + 'Select the recipient/s of file access', + ), + + // Show Select Recipient Buttons + SelectRecipients( + isExternalRes: widget.isExternalRes, + recipientTypeList: widget.recipientTypeList, + setPublicFunction: _setRecipientsToPublic, + setAuthUsersFunction: _setRecipientsToAuthUsers, + setIndividualFunction: _setRecipientsToIndividual, + setGroupFunction: _setRecipientsToGroup, + updateIndWebIdFunction: updateIndWebIdInput, + updateGroupWebIdFunction: updateGroupWebIdInput, + ), + + // List selected recipient webids or recipient + // type (public/auth) + ShowSelectedRecipients( + selectedRecipientType: selectedRecipientType, + selectedRecipientDetails: selectedRecipientDetails, + ), + + smallGapV, + getHeading( + 'Select one or more file access permissions', + ), + // Show access mode checkboxes and update + // selection status on click + ...getPermissionCheckBoxes( + accessModeList, + modeSwitches: { + AccessMode.read: readChecked, + AccessMode.write: writeChecked, + AccessMode.control: controlChecked, + AccessMode.append: appendChecked, + }, + onUpdate: updateCheckbox, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () async { + // Grant Permission and update permission map + // used by permission table + + if (selectedRecipientType.type.isNotEmpty) { + if (selectedPermList.isNotEmpty) { + SolidFunctionCallStatus result; + try { + // Update ACL and permission logs to grant permission + result = await grantPermission( + fileName: widget.resourceName, + isFile: widget.isFile, + permissionList: selectedPermList, + recipientType: selectedRecipientType, + recipientWebIdList: finalWebIdList, + ownerWebId: widget.ownerWebId, + granterWebId: widget.granterWebId, + isExternalRes: widget.isExternalRes, + groupName: selectedRecipientType == RecipientType.group + ? groupNameController.text.trim() + : null, + ); + + // Close grant permission dialog + if (!context.mounted) return; + Navigator.of(context).pop(); + } on Object catch (e, stackTrace) { + result = SolidFunctionCallStatus.fail; + debugPrintException(e, stackTrace); + } + + if (result == SolidFunctionCallStatus.success) { + _showSnackBar(successMsg, ActionColors.success); + // Update permissions table + await widget.updatePermissionsFunction( + widget.resourceName, //_resourceName, + isFile: widget.isFile, + isExternalRes: widget.isExternalRes, + ); + + // Mark permissions as granted successfully for callback tracking + await widget.updatePermissionGrantedFunction(); + + // Trigger the onPermissionGranted callback if provided + widget.onPermissionGranted?.call(); + } else if (result == SolidFunctionCallStatus.fail) { + // More detailed error message with troubleshooting tips + _showSnackBar(failureMsg, ActionColors.error); + + // Also log to console for debugging + debugPrintFailure( + widget.resourceName, // _resourceName, + finalWebIdList, + selectedPermList, + ); + } else if (result == SolidFunctionCallStatus.notInitialised) { + _showSnackBar(podNotInitMsg, ActionColors.warning); + } else { + await _alert(updatePermissionMsg); + } + } else { + await _alert( + 'Please select one or more file access permissions', + ); + } + } else { + await _alert('Please select a type of recipient'); + } + }, + child: const Text('Grant Permission'), + ), + TextButton( + onPressed: () { + // Close dialog + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + ], + ); + } +} diff --git a/lib/src/solid/grant_permission_helper.dart b/lib/src/solid/grant_permission_helper.dart index da34bdaf..f977cf4a 100644 --- a/lib/src/solid/grant_permission_helper.dart +++ b/lib/src/solid/grant_permission_helper.dart @@ -28,9 +28,6 @@ library; import 'package:flutter/material.dart'; -import 'package:markdown_tooltip/markdown_tooltip.dart'; - -import 'package:solidpod/src/solid/constants/ui.dart'; import 'package:solidpod/src/solid/constants/web_acl.dart'; import 'package:solidpod/src/solid/utils/heading.dart'; import 'package:solidpod/src/widgets/permission_checkbox.dart'; @@ -128,9 +125,16 @@ const granterRecipientTypes = [ RecipientType.group, ]; -String getWelcomeStr(String? fileName) => fileName != null - ? 'Share $fileName resource with other PODs' - : 'Share your data resources with other PODs'; +/// Get title of sharing page +String getSharingTitleStr({ + String? fileName, + bool isFile = false, +}) => + fileName != null + ? isFile + ? 'Share $fileName' + : 'Share $fileName folder' + : 'Share your data with other user\'s PODs'; Widget getHeading(String text) => buildHeading( text, @@ -153,39 +157,6 @@ List getPermissionCheckBoxes( permissionCheckbox(mode, modeSwitches[mode]!, onUpdate), ]; -Widget getButton( - String text, { - required void Function() onPressed, -}) => - Padding( - padding: const EdgeInsets.all(8), - child: ElevatedButton( - onPressed: onPressed, - child: Text(text), - ), - ); - -Widget getRecipientTypeButton( - RecipientType recipientType, { - required void Function() onPressed, - EdgeInsetsGeometry? padding, -}) { - assert(recipientType != RecipientType.none); - return Expanded( - child: Container( - padding: padding, - height: 50, - child: MarkdownTooltip( - message: recipientToolTips[recipientType]!, - child: ElevatedButton( - onPressed: onPressed, - child: Text(recipientType.description), - ), - ), - ), - ); -} - Widget getResourceForm({ required TextEditingController formController, required bool isFile, @@ -226,138 +197,3 @@ Widget getResourceForm({ ], ), ); - -Widget getRecipientText(RecipientType recipientType, String recipientDetails) => - Container( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - const Text( - 'Recipient/s: ', - style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500), - ), - Flexible( - child: Text( - '${recipientType.type}${recipientDetails.isEmpty ? "" : " ($recipientDetails)"}', - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - // 20251008 gjw Choose blue rather than - // orange which looks red. The red looks - // like it is an error. Blue is more - // neutral. - color: Colors.blueAccent, - ), - ), - ), - ], - ), - ); - -Scrollbar getScrollbar({ - required ScrollController controller, - required Axis direction, - required Widget child, -}) => - Scrollbar( - // 20250722 jm: - // For scrollbar visibility before scrolling, - // set to true, or set property to true - // in parent app MaterialApp(theme: ThemeData(scrollbarTheme: scrollbarTheme: ScrollbarThemeData( - // thumbVisibility: WidgetStateProperty.all(true))) - thumbVisibility: true, // show before user starts scrolling - controller: controller, - child: SingleChildScrollView( - controller: controller, - scrollDirection: direction, - child: child, - ), - ); - -Scrollbar getFormScrollbar(ScrollController controller, Widget permDataTable) => - getScrollbar( - controller: controller, - direction: Axis.horizontal, - child: Column( - children: [ - Row( - children: [ - permDataTable, - // Hspace to avoid vertical scrollbar overlap with table - ScrollbarLayout.horizontalGap, - ], - ), - // Vspace to avoid horizontal scrollbar overlap of table - ScrollbarLayout.verticalGap, - ], - ), - ); - -Scrollbar getPageScrollbar(ScrollController controller, Widget form) => - getScrollbar( - controller: controller, - direction: Axis.vertical, - child: Column( - children: [ - smallGapV, - form, - ], - ), - ); - -Container getButtonContainer({required List buttons}) => Container( - padding: const EdgeInsets.all(8.0), - height: 100, - child: Row( - children: - // av 20250526: - // Public and Authenticated users buttons are - // disabled in this function at the moment because - // providing public or authenticated permissions to - // external resources is not yet implemented in - // [grantPermission()] function. - buttons, - ), - ); - -// ElevatedButton getRetrieveButton( -// BuildContext context, -// String fileName, -// bool isFile, { -// required Future Function( -// String, { -// bool isFile, -// }) onRetrieve, -// }) => -// ElevatedButton( -// child: const Text('Retrieve permissions'), -// onPressed: () async { -// if (fileName.isEmpty) { -// await alert(context, 'Please enter a file name'); -// } else { -// await onRetrieve(fileName, isFile: isFile); -// } -// }, -// ); - -Form getForm({ - required Key formKey, - required Widget welcomeHeading, - required List children, -}) => - Form( - key: formKey, - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Column( - children: [ - welcomeHeading, - smallGapV, - Column( - mainAxisSize: MainAxisSize.min, - children: children, - ), - ], - ), - ), - ); diff --git a/lib/src/solid/grant_permission_ui.dart b/lib/src/solid/grant_permission_ui.dart index 596c1c97..7107f856 100644 --- a/lib/src/solid/grant_permission_ui.dart +++ b/lib/src/solid/grant_permission_ui.dart @@ -35,22 +35,20 @@ import 'package:flutter/material.dart'; import 'package:solidpod/src/solid/chk_exists_and_has_acl.dart'; import 'package:solidpod/src/solid/constants/ui.dart'; import 'package:solidpod/src/solid/constants/web_acl.dart'; -import 'package:solidpod/src/solid/grant_permission.dart'; import 'package:solidpod/src/solid/grant_permission_helper.dart'; +import 'package:solidpod/src/solid/models/permission_details.dart'; +import 'package:solidpod/src/solid/permission_table.dart'; import 'package:solidpod/src/solid/read_permission.dart'; +import 'package:solidpod/src/solid/share_resource_button.dart'; import 'package:solidpod/src/solid/solid_func_call_status.dart'; import 'package:solidpod/src/solid/utils/alert.dart'; -import 'package:solidpod/src/solid/utils/authdata_manager.dart'; +import 'package:solidpod/src/solid/utils/get_authoriser.dart'; import 'package:solidpod/src/solid/utils/heading.dart'; -import 'package:solidpod/src/solid/utils/snack_bar.dart'; import 'package:solidpod/src/widgets/app_bar.dart'; -import 'package:solidpod/src/widgets/file_permission_data_table.dart'; -import 'package:solidpod/src/widgets/group_webid_input_dialog.dart'; -import 'package:solidpod/src/widgets/ind_webid_input_dialog.dart'; import 'package:solidpod/src/widgets/loading_screen.dart'; /// A [StatefulWidget] for showing and editing access permissions to a -/// file. It displays the permission table of users with access, and +/// resource. It displays the permission table of users with access, and /// allows the user to change access permissions: by granting access /// to others, changing a recipients access permissions or revoking /// access permissions. @@ -73,8 +71,6 @@ import 'package:solidpod/src/widgets/loading_screen.dart'; /// - [onNavigateBack] - Callback function called when navigating back from the screen. class GrantPermissionUi extends StatefulWidget { - /// Initialise widget variables. - const GrantPermissionUi({ required this.child, this.title = 'Demonstrating data sharing functionality', @@ -136,8 +132,7 @@ class GrantPermissionUi extends StatefulWidget { final List accessModeList; - /// The list of types of recipients receiving permission to access the resource. By default all four - /// types of recipient are listed. + /// The list of types of recipients receiving permission to access the resource. By default all four types of recipient are listed. final List recipientTypeList; @@ -185,30 +180,6 @@ class GrantPermissionUi extends StatefulWidget { class GrantPermissionUiState extends State with SingleTickerProviderStateMixin { - /// read permission checked flag - - bool readChecked = false; - - /// write permission checked flag - - bool writeChecked = false; - - /// control permission checked flag - - bool controlChecked = false; - - /// append permission checked flag - - bool appendChecked = false; - - /// Public permission check flag - - bool publicChecked = false; - - /// WebId textfield enable/disable flag - - bool webIdTextFieldEnabled = true; - /// Flag to check whether page is initialised. bool pageInitialied = false; @@ -221,22 +192,10 @@ class GrantPermissionUiState extends State List recipientTypeList = []; - /// Form controller - - final formKey = GlobalKey(); - /// Filename text controller final fileNameController = TextEditingController(); - /// Group name text controller - - final groupNameController = TextEditingController(); - - /// Group of webIds text controller - - final groupWebIdsController = TextEditingController(); - /// Permission data map of a file Map permDataMap = {}; @@ -253,37 +212,20 @@ class GrantPermissionUiState extends State String permDataFile = ''; - /// Selected recipient - - RecipientType selectedRecipientType = RecipientType.none; - - /// Selected recipient details - - String selectedRecipientDetails = ''; - - /// List of webIds for group permission - - List? finalWebIdList; - - /// Selected list of permissions - - List selectedPermList = []; - /// Flag to track if permissions were granted successfully. bool permissionsGrantedSuccessfully = false; /// Pod data list retreived as a Future - late Future> podDataList; + late Future podDataList; /// A flag to identify if the resource is a file or not bool isFile = true; - /// Runs multiple asynchronous functions to get the data from - /// POD server if necessary. + /// Gets permission details data from POD server if necessary. - Future> loadPodData( + Future loadPodData( String resName, { bool isFile = true, bool isExternalRes = false, @@ -291,31 +233,34 @@ class GrantPermissionUiState extends State final SolidFunctionCallStatus response = await chkExistsAndHasAcl( fileName: resName, isFile: isFile, - isExternalRes: widget.isExternalRes, + isExternalRes: isExternalRes, ); switch (response) { case SolidFunctionCallStatus.aclFound: + + // Permission map from ACL of resource final Map result = await readPermission( fileName: resName, isFile: isFile, - isExternalRes: widget.isExternalRes, + isExternalRes: isExternalRes, ); - // Fetch owner's webID - // ownerWebId == userWebId if not externally owned resource - - final ownerWebId = widget.isExternalRes - ? widget.ownerWebId - : await AuthDataManager.getWebId(); - // Fetch granter's webID - // granterWebId == userWebId if not externally owned resource - - final granterWebId = widget.isExternalRes - ? widget.granterWebId - : await AuthDataManager.getWebId(); + // Permission Details object to store permission map from ACL, and owner + // and granter of a resource. + final permissionDetails = PermissionDetails( + permissionMap: result, + ownerWebId: await getAuthoriser( + isExternalRes: isExternalRes, + webId: widget.ownerWebId, + ), + granterWebId: await getAuthoriser( + isExternalRes: isExternalRes, + webId: widget.granterWebId, + ), + ); - return [result, ownerWebId, granterWebId]; + return permissionDetails; case SolidFunctionCallStatus.notLoggedIn: await _alert('Please login first to retrieve permission'); @@ -326,13 +271,14 @@ class GrantPermissionUiState extends State default: await _alert('Unknown error'); } - return []; + + return null; } @override void initState() { super.initState(); - // Load future + // Load permission map from ACL, owner and granter web ids if (widget.resourceName != null) { podDataList = loadPodData( widget.resourceName as String, @@ -340,19 +286,10 @@ class GrantPermissionUiState extends State isExternalRes: widget.isExternalRes, ); } - - // Load access mode list to be displayed - for (final accessModeStr in widget.accessModeList) { - accessModeList.add(getAccessMode(accessModeStr)); - } - - // Load recipient type list to be displayed - for (final recTypeStr in widget.recipientTypeList) { - recipientTypeList.add(RecipientType.getInstanceByValue(recTypeStr)); - } } - // Get new permission and update the permission map + /// Update the permission data map + Future _updatePermissions( String fileName, { bool isFile = true, @@ -363,85 +300,38 @@ class GrantPermissionUiState extends State isFile: isFile, isExternalRes: isExternalRes, ); - if (pdata.isNotEmpty) { - assert(pdata.length == 3); - final permissionMap = pdata.first; - final ownerWebId = pdata[1]; - final granterWebId = pdata.last; - - if (permissionMap.isEmpty) { - await _alert('We could not find a resource by the name $fileName'); - } else { - setState(() { - permDataMap = permissionMap; - permDataFile = fileName; - _ownerWebId = ownerWebId as String; - _granterWebId = granterWebId as String; - }); - } - } - } - - // Update checkbox tick data. - - void _updateCheckbox(bool newValue, AccessMode accessMode) => setState(() { - switch (accessMode) { - case AccessMode.read: - readChecked = newValue; - case AccessMode.write: - writeChecked = newValue; - case AccessMode.control: - controlChecked = newValue; - case AccessMode.append: - appendChecked = newValue; - } - if (newValue) { - selectedPermList.add(accessMode.mode); - } else { - selectedPermList.remove(accessMode.mode); - } - }); - // Update individual webid input data - void _updateIndWebIdInput(String receiverWebId) => setState(() { - selectedRecipientType = RecipientType.individual; - selectedRecipientDetails = receiverWebId; - finalWebIdList = [receiverWebId]; - }); + assert(pdata != null); - // Update group of webids input data - void _updateGroupWebIdInput(String groupName, List webIdList) => + if (pdata!.permissionMap.isEmpty) { + await _alert('We could not find a resource by the name $fileName'); + } else { setState(() { - selectedRecipientType = RecipientType.group; - selectedRecipientDetails = - '$groupName with WebIDs ${webIdList.join(', ')}'; - finalWebIdList = webIdList; + permDataMap = pdata.permissionMap; + permDataFile = fileName; + _ownerWebId = pdata.ownerWebId; + _granterWebId = pdata.granterWebId; }); + } + // } + } - // Private function to call alert dialog in grant permission UI context + /// Private function to call alert dialog in grant permission UI context Future _alert(String msg) async => alert(context, msg); - // Private function to show snackbar in grant permission UI context - Future _showSnackBar( - String msg, - Color bgColor, { - Duration duration = const Duration(seconds: 4), - }) async => - showSnackBar(context, msg, bgColor, duration: duration); - /// Build the main widget - Widget _buildPermPage(BuildContext context, [List? futureObjList]) { + Widget _buildPermPage( + BuildContext context, [ + PermissionDetails? futurePermDetails, + ]) { /// Controller for vertical page scrolling final pageScrollController = ScrollController(); - /// Controller for horizontal permissions table scrolling - final tableScrollController = ScrollController(); - // Check if future is set or not. If set display the permission map - if (futureObjList != null && pageInitialied == false) { - permDataMap = futureObjList.first as Map; - _ownerWebId = futureObjList[1] as String; - _granterWebId = futureObjList.last as String; + if (futurePermDetails != null && pageInitialied == false) { + permDataMap = futurePermDetails.permissionMap; + _ownerWebId = futurePermDetails.ownerWebId; + _granterWebId = futurePermDetails.granterWebId; permDataFile = widget.resourceName!; pageInitialied = true; } @@ -456,175 +346,14 @@ class GrantPermissionUiState extends State await _updatePermissions( fileName, isFile: isFile, - isExternalRes: widget.isExternalRes, ); } }, ); - final recipientTypeActions = { - RecipientType.public: () => setState(() { - selectedRecipientType = RecipientType.public; - selectedRecipientDetails = ''; - finalWebIdList = [publicAgent.value]; - }), - RecipientType.authUser: () => setState(() { - selectedRecipientType = RecipientType.authUser; - selectedRecipientDetails = ''; - finalWebIdList = [authenticatedAgent.value]; - }), - RecipientType.individual: () async => await indWebIdInputDialog( - context, - _updateIndWebIdInput, - widget.dataFilesMap, - ), - RecipientType.group: () async => await groupWebIdInputDialog( - context, - groupNameController, - groupWebIdsController, - _updateGroupWebIdInput, - ), - }; - - // 20260109 jesscmoore Added capability for granters to share - // resources, as well as resource owners - final recipientButtonContainer = getButtonContainer( - buttons: widget.isExternalRes - // Recipient type buttons for resource granter - ? [ - for (final rtype in granterRecipientTypes) - if (recipientTypeList.contains(rtype)) - getRecipientTypeButton( - rtype, - onPressed: recipientTypeActions[rtype]!, - padding: getPadding(rtype), - ), - ] - // Recipient type buttons for resource owner - : [ - for (final rtype in ownerRecipientTypes) - if (recipientTypeList.contains(rtype)) - getRecipientTypeButton( - rtype, - onPressed: recipientTypeActions[rtype]!, - padding: getPadding(rtype), - ), - ], - ); - bool getIsFile() => widget.resourceName != null ? widget.isFile : isFile; - final permDataTable = buildPermDataTable( - context: context, - permDataResource: permDataFile, - isFile: getIsFile(), - permDataMap: permDataMap, - ownerWebId: _ownerWebId, - granterWebId: _granterWebId, - parentWidget: widget.child, - updatePermissionsFunction: _updatePermissions, - isExternalRes: widget.isExternalRes, - ); - - final grantPermissionButton = getButton( - 'Grant Permission', - onPressed: () async { - if (formKey.currentState!.validate()) { - if (selectedRecipientType.type.isNotEmpty) { - if (selectedPermList.isNotEmpty) { - final dataFile = widget.resourceName ?? fileNameController.text; - - SolidFunctionCallStatus result; - try { - result = await grantPermission( - fileName: dataFile, - isFile: getIsFile(), - permissionList: selectedPermList, - recipientType: selectedRecipientType, - recipientWebIdList: finalWebIdList as List, - ownerWebId: _ownerWebId, - granterWebId: _granterWebId, - isExternalRes: widget.isExternalRes, - groupName: selectedRecipientType == RecipientType.group - ? groupNameController.text.trim() - : null, - ); - } on Object catch (e, stackTrace) { - result = SolidFunctionCallStatus.fail; - debugPrintException(e, stackTrace); - } - - if (result == SolidFunctionCallStatus.success) { - _showSnackBar(successMsg, ActionColors.success); - await _updatePermissions( - dataFile, - isFile: getIsFile(), - isExternalRes: widget.isExternalRes, - ); - - // Mark permissions as granted successfully for callback tracking - setState(() => permissionsGrantedSuccessfully = true); - - // Trigger the onPermissionGranted callback if provided - widget.onPermissionGranted?.call(); - } else if (result == SolidFunctionCallStatus.fail) { - // More detailed error message with troubleshooting tips - _showSnackBar(failureMsg, ActionColors.error); - - // Also log to console for debugging - debugPrintFailure(dataFile, finalWebIdList, selectedPermList); - } else if (result == SolidFunctionCallStatus.notInitialised) { - _showSnackBar(podNotInitMsg, ActionColors.warning); - } else { - await _alert(updatePermissionMsg); - } - } else { - await _alert('Please select one or more file access permissions'); - } - } else { - await _alert('Please select a type of recipient'); - } - } - }, - ); - - final form = getForm( - formKey: formKey, - welcomeHeading: - buildHeading(getWelcomeStr(widget.resourceName), 22, Colors.blueGrey), - children: [ - if (widget.resourceName == null) ...[ - getResourceForm( - formController: fileNameController, - isFile: isFile, - onResourceTypeChange: (bool v) => setState(() => isFile = v), - ), - smallGapV, - retrievePermissionButton, - ], - largeGapV, - getHeading('Select the recipient/s of file access permissions'), - getRecipientText(selectedRecipientType, selectedRecipientDetails), - recipientButtonContainer, - smallGapV, - getHeading('Select the list of file access permissions'), - ...getPermissionCheckBoxes( - accessModeList, - modeSwitches: { - AccessMode.read: readChecked, - AccessMode.write: writeChecked, - AccessMode.control: controlChecked, - AccessMode.append: appendChecked, - }, - onUpdate: _updateCheckbox, - ), - grantPermissionButton, - largeGapV, - getHeading('Granted file access permissions'), - getFormScrollbar(tableScrollController, permDataTable), - ], - ); - + // Use customAppBar if provided final customAppBar = widget.customAppBar ?? defaultAppBar( context, @@ -636,27 +365,89 @@ class GrantPermissionUiState extends State ); return Scaffold( + // Display app bar if showAppBar selected + // AppBar will be defaultAppBar() if customAppBar() + // not provided appBar: widget.showAppBar ? customAppBar : null, - // Make Grant Permission UI vertically scrollable - // Shows when content exceeds display height - body: getPageScrollbar(pageScrollController, form), + body: Scrollbar( + thumbVisibility: true, // show before user starts scrolling + controller: pageScrollController, + child: SingleChildScrollView( + controller: pageScrollController, + scrollDirection: Axis.vertical, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + children: [ + smallGapV, + // Sharing heading + buildHeading( + getSharingTitleStr( + fileName: widget.resourceName, + isFile: widget.isFile, + ), + 22, + Colors.blueGrey, + ), + smallGapV, + // Choose resource and run _updatePermissions if + // resourceName not provided + if (widget.resourceName == null) ...[ + getResourceForm( + formController: fileNameController, + isFile: isFile, + onResourceTypeChange: (bool v) => + setState(() => isFile = v), + ), + if (permDataMap.isEmpty) ...[ + smallGapV, + retrievePermissionButton, + ], + ], + ShareResourceButton( + resourceName: widget.resourceName, + fileNameController: fileNameController, + accessModeList: widget.accessModeList, + recipientTypeList: widget.recipientTypeList, + updatePermissionsFunction: _updatePermissions, + ownerWebId: _ownerWebId, + granterWebId: _granterWebId, + isExternalRes: widget.isExternalRes, + isFile: widget.isFile, + dataFilesMap: widget.dataFilesMap, + onPermissionGranted: widget.onPermissionGranted, + ), + + largeGapV, + getHeading('Current access permissions'), + PermissionTable( + resourceName: permDataFile, + permDataMap: permDataMap, + ownerWebId: _ownerWebId, + granterWebId: _granterWebId, + updatePermissionsFunction: _updatePermissions, + parentWidget: widget.child, + isFile: getIsFile(), + isExternalRes: widget.isExternalRes, + ), + ], + ), + ), + ), + ), ); } @override - Widget build(BuildContext context) => - // Build as a separate widget with the possibility of adding a FutureBuilder - // in the Future - - widget.resourceName == null - ? _buildPermPage(context) - : FutureBuilder( - future: podDataList, - builder: (context, snapshot) => snapshot.hasData - ? snapshot.data!.first == SolidFunctionCallStatus.notLoggedIn - ? widget.child - : _buildPermPage(context, snapshot.data) - : Scaffold(body: loadingScreen(normalLoadingScreenHeight)), - ); + Widget build(BuildContext context) => widget.resourceName == null + ? _buildPermPage(context) + : FutureBuilder( + future: podDataList, + builder: (context, snapshot) => snapshot.hasData + ? snapshot.data!.permissionMap.isEmpty + ? widget.child + : _buildPermPage(context, snapshot.data) + : Scaffold(body: loadingScreen(normalLoadingScreenHeight)), + ); } diff --git a/lib/src/solid/models/permission_details.dart b/lib/src/solid/models/permission_details.dart new file mode 100644 index 00000000..11f9f55f --- /dev/null +++ b/lib/src/solid/models/permission_details.dart @@ -0,0 +1,44 @@ +/// Data model for permission details +/// +/// Copyright (C) 2024, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Jess Moore + +library; + +/// Data model for permission details retrieved in GrantPermissionUi() +/// from ACL file, and fetching ownerWebId and granterWebId if provided or deriving them. + +class PermissionDetails { + final Map permissionMap; + final String ownerWebId; + final String granterWebId; + + const PermissionDetails({ + required this.permissionMap, + required this.ownerWebId, + required this.granterWebId, + }); +} diff --git a/lib/src/solid/permission_table.dart b/lib/src/solid/permission_table.dart new file mode 100644 index 00000000..2c8dfd96 --- /dev/null +++ b/lib/src/solid/permission_table.dart @@ -0,0 +1,228 @@ +/// Table listing permissions of a resource. +/// +// Time-stamp: +/// +/// Copyright (C) 2024-2025, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Jess Moore, Anushka Vidanage + +library; + +import 'package:flutter/material.dart'; + +import 'package:solidpod/src/solid/constants/common.dart'; +import 'package:solidpod/src/solid/constants/ui.dart'; +import 'package:solidpod/src/solid/constants/web_acl.dart'; +import 'package:solidpod/src/solid/revoke_permission_button.dart'; + +/// A [StatefulWidget] for listing the permissions of a resource. +/// +/// Parameters: +/// - [resourceName] - The filename or file url of the resource. If [isExternalRes], it should be the url of the resource. +/// - [permDataMap] is the map of permission data for the [resourceName] +/// - [ownerWebId] - WebId of the owner of the resource. Required if the resource is externally owned. +/// - [granterWebId] - WebId of the granter of the resource. Required if the resource is externally owned. +/// - [isFile] - Boolean flag describing whether the resource is a file. If false, the resource is assumed to be a directory. +/// - [isExternalRes] - Boolean flag describing whether the resource +/// is externally owned. +/// - [updatePermissionsFunction] is the function to be called to refresh the permission table. +/// - [parentWidget] is the widget to return to after an action Eg: deletion of a +/// permission +/// + +class PermissionTable extends StatefulWidget { + /// The name of the file or directory for which permissions are being + /// shown. + + final String resourceName; + + /// Map of access permission data being displayed for [resourceName]. + + final Map permDataMap; + + /// WebId of the resource owner. + + final String ownerWebId; + + /// WebId of the user granting/revoking access to the resource. + + final String granterWebId; + + /// A flag denoting whether resource is externally owned. + + final bool isExternalRes; + + /// A flag to determine whether the given resource is a file or not. + + final bool isFile; + + /// Function run to update permissions table + + final Function updatePermissionsFunction; + + /// Parent widget to return to. + + final Widget parentWidget; + + const PermissionTable({ + super.key, + required this.resourceName, + required this.permDataMap, + required this.ownerWebId, + required this.granterWebId, + required this.updatePermissionsFunction, + required this.parentWidget, + required this.isFile, + this.isExternalRes = false, + }); + + @override + State createState() => _PermissionTableState(); +} + +class _PermissionTableState extends State { + /// Controller for horizontal permissions table scrolling + final tableScrollController = ScrollController(); + + @override + void initState() { + super.initState(); + } + + DataColumn buildDataColumn(String title, String tooltip) { + return DataColumn( + label: Expanded( + child: Center( + child: Text( + title, + ), + ), + ), + tooltip: tooltip, + ); + } + + @override + Widget build(BuildContext context) { + // Make wide permission table horizontally scrollable + // Shows when content exceeds display width + return Scrollbar( + // 20250722 jm: + // For scrollbar visibility before scrolling, + // set to true, or set property to true + // in parent app MaterialApp(theme: ThemeData(scrollbarTheme: scrollbarTheme: ScrollbarThemeData( + // thumbVisibility: WidgetStateProperty.all(true))) + thumbVisibility: true, // show before user starts scrolling + controller: tableScrollController, + child: SingleChildScrollView( + controller: tableScrollController, + scrollDirection: Axis.horizontal, + child: Column( + children: [ + Row( + children: [ + DataTable( + columns: [ + buildDataColumn( + 'Receiver', + 'WebID of the permission recipient', + ), + buildDataColumn('Receiver type', 'Type of the receiver'), + buildDataColumn('Permissions', 'List of permissions given'), + buildDataColumn('Actions', 'Delete permission'), + ], + // receiverWebId is the webId of each individual with access to the file + rows: widget.permDataMap.keys.map((receiverWebId) { + return DataRow( + cells: [ + DataCell( + Container( + padding: const EdgeInsets.fromLTRB(0, 10, 0, 0), + //width: cWidth, + child: Column( + children: [ + SelectableText( + (receiverWebId.replaceAll('.ttl', '')) + as String, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + DataCell( + Text( + getRecipientType( + widget.permDataMap[receiverWebId][agentStr] + as String, + receiverWebId as String, + ).description, + ), + ), + DataCell( + Text( + (widget.permDataMap[receiverWebId][permStr] as List) + .join(', '), + ), + ), + // If recipient != owner, then show the delete permission button + if (widget.ownerWebId != receiverWebId) ...[ + DataCell( + // Revoke permissions icon button + RevokePermissionButton( + resourceName: widget.resourceName, + permDataMap: widget.permDataMap, + receiverWebId: receiverWebId, + ownerWebId: widget.ownerWebId, + granterWebId: widget.granterWebId, + isFile: widget.isFile, + isExternalRes: widget.isExternalRes, + updatePermissionsFunction: + widget.updatePermissionsFunction, + ), + ), + ] else ...[ + const DataCell( + Text(''), + ), + ], + ], + ); + }).toList(), + ), + // Hspace to avoid vertical scrollbar overlap with table + ScrollbarLayout.horizontalGap, + ], + ), + // Vspace to avoid horizontal scrollbar overlap of table + ScrollbarLayout.verticalGap, + ], + ), + ), + ); + } +} diff --git a/lib/src/solid/revoke_permission_button.dart b/lib/src/solid/revoke_permission_button.dart new file mode 100644 index 00000000..d0653864 --- /dev/null +++ b/lib/src/solid/revoke_permission_button.dart @@ -0,0 +1,180 @@ +/// A button for revoking permission. +/// +// Time-stamp: +/// +/// Copyright (C) 2024-2025, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Jess Moore, Anushka Vidanage + +library; + +import 'package:flutter/material.dart'; + +import 'package:solidpod/src/solid/constants/common.dart'; +import 'package:solidpod/src/solid/constants/web_acl.dart'; +import 'package:solidpod/src/solid/revoke_permission.dart'; +import 'package:solidpod/src/solid/utils/snack_bar.dart'; + +/// A [StatefulWidget] for the revoke permission icon button. Updates +/// owner's ACL for resource, updates owner, granter, recipient logs, +/// and calls updatePermissions() to refresh permission table data. +/// +/// Parameters: +/// - [resourceName] - The filename or file url of the resource. If [isExternalRes], it should be the url of the resource. +/// - [permDataMap] is the map of permission data for the [resourceName] +/// - [ownerWebId] - WebId of the owner of the resource. Required if the resource is externally owned. +/// - [granterWebId] - WebId of the granter of the resource. Required if the resource is externally owned. +/// - [receiverWebId] - WebId with access to the resource, one of ownerWebId, granterWebId or recipientWebId. +/// - [isFile] - Boolean flag describing whether the resource is a file. If false, the resource is assumed to be a directory. +/// - [isExternalRes] - Boolean flag describing whether the resource +/// is externally owned. +/// - [updatePermissionsFunction] is the function to be called to refresh the permission table. +/// + +class RevokePermissionButton extends StatefulWidget { + /// The name of the file or directory for which permissions are being + /// shown. + + final String resourceName; + + /// Map of access permission data being displayed for [resourceName]. + + final Map permDataMap; + + /// WebId with access to resource. + + final String receiverWebId; + + /// WebId of the resource owner. + + final String ownerWebId; + + /// WebId of the user granting/revoking access to the resource. + + final String granterWebId; + + /// A flag denoting whether resource is externally owned. + + final bool isExternalRes; + + /// A flag to determine whether the given resource is a file or not. + + final bool isFile; + + /// Function run to update permissions table + + final Function updatePermissionsFunction; + + const RevokePermissionButton({ + super.key, + required this.resourceName, + required this.permDataMap, + required this.receiverWebId, + required this.ownerWebId, + required this.granterWebId, + required this.updatePermissionsFunction, + required this.isFile, + this.isExternalRes = false, + }); + + @override + State createState() => _RevokePermissionButtonState(); +} + +class _RevokePermissionButtonState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return IconButton( + icon: const Icon( + Icons.delete, + size: 24.0, + color: Colors.red, + ), + onPressed: () { + showDialog( + context: context, + builder: (ctx) { + return AlertDialog( + title: const Text('Please Confirm'), + content: Text( + 'Are you sure you want to remove the [${(widget.permDataMap[widget.receiverWebId][permStr] as List).join(', ')}] permission/s from ${widget.receiverWebId.replaceAll('.ttl', '')}?', + ), + actions: [ + // The "Yes" button + TextButton( + onPressed: () async { + await revokePermission( + fileName: widget.resourceName, + isFile: widget.isFile, + permissionList: widget.permDataMap[widget.receiverWebId] + [permStr] as List, + recipientIndOrGroupWebId: widget.receiverWebId, + ownerWebId: widget.ownerWebId, + granterWebId: widget.granterWebId, + recipientType: getRecipientType( + widget.permDataMap[widget.receiverWebId][agentStr] + as String, + widget.receiverWebId, + ), + isExternalRes: widget.isExternalRes, + ); + + if (ctx.mounted) { + Navigator.pop(ctx); + } + if (ctx.mounted) { + showSnackBar( + context, + 'Permission revoked successfully!', + Colors.red, + ); + } + await widget.updatePermissionsFunction( + widget.resourceName, + isFile: widget.isFile, + ); + }, + child: const Text('Yes'), + ), + TextButton( + onPressed: () { + // Close the dialog + Navigator.of(ctx).pop(); + }, + child: const Text('No'), + ), + ], + ); + }, + ); + }, + ); + } +} diff --git a/lib/src/solid/select_recipients.dart b/lib/src/solid/select_recipients.dart new file mode 100644 index 00000000..57bd409d --- /dev/null +++ b/lib/src/solid/select_recipients.dart @@ -0,0 +1,205 @@ +/// Button list for selecting recipients. +/// +// Time-stamp: +/// +/// Copyright (C) 2024-2025, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Jess Moore, Anushka Vidanage + +library; + +import 'package:flutter/material.dart'; + +import 'package:markdown_tooltip/markdown_tooltip.dart'; + +import 'package:solidpod/src/solid/constants/web_acl.dart'; +import 'package:solidpod/src/solid/grant_permission_helper.dart'; + +/// A [StatefulWidget] for a container of buttons for +/// selecting recipients in the grant permission form. +/// +/// Parameters: +/// - [isExternalRes] - Boolean flag describing whether +/// the resource is externally owned. +/// - [recipientTypeList] - List of recipient type options to show. +/// - [setPublicFunction] - a function for setting recipients to +/// the public. +/// - [setAuthUsersFunction] - a function for setting recipients +/// to all authorised users. +/// - [setIndividualFunction] - a function for selecting an +/// individual recipient webId. +/// - [setGroupFunction] - a function for selecting a +/// group of webIds as recipients. +/// - [updateIndWebIdFunction] - a function to update the selected +/// individual webId. +/// - [updateGroupWebIdFunction] - a function to update the selected +/// group webId list and group name. + +class SelectRecipients extends StatefulWidget { + /// A flag denoting whether the resource is externally owned. + + final bool isExternalRes; + + /// The list of types of recipients to show in form. By default + /// all four types of recipient are listed. + + final List recipientTypeList; + + /// Map of data files on a user's POD used to extract the + /// user's recipient list by the WebIdTextInputScreen. + /// If not provided, the WebIdTextInputScreen will read the + /// user's files in their app data folder on their Pod to + /// fetch the ACLs needed to derive the user's recipient list. + + final Map dataFilesMap; + + /// A function for setting recipients to the public. + + final Function setPublicFunction; + + /// A function for setting recipients to all authorised users. + + final Function setAuthUsersFunction; + + /// A function for selecting an individual recipient webId. + + final Function setIndividualFunction; + + /// A function for selecting a group of webIds as recipients. + + final Function setGroupFunction; + + /// A function to update the selected individual webId. + /// + final Function updateIndWebIdFunction; + + /// A function to update the selected group webId list and group + /// name. + + final Function updateGroupWebIdFunction; + + const SelectRecipients({ + super.key, + required this.isExternalRes, + required this.recipientTypeList, + required this.setPublicFunction, + required this.setAuthUsersFunction, + required this.setIndividualFunction, + required this.setGroupFunction, + required this.updateIndWebIdFunction, + required this.updateGroupWebIdFunction, + this.dataFilesMap = const {}, + }); + + @override + State createState() => _SelectRecipientsState(); +} + +class _SelectRecipientsState extends State { + /// Define recipient type list + + List recipientTypeList = []; + + @override + void initState() { + super.initState(); + + // Load recipient type list to be displayed + for (final recTypeStr in widget.recipientTypeList) { + recipientTypeList.add(RecipientType.getInstanceByValue(recTypeStr)); + } + } + + @override + void dispose() { + super.dispose(); + } + + List selectRecipientButtons({ + required List allowedRecipientTypes, + }) { + return [ + for (final rtype in allowedRecipientTypes) + if (recipientTypeList.contains(rtype)) + Expanded( + child: Container( + padding: getPadding(rtype), + height: 50, + child: MarkdownTooltip( + message: recipientToolTips[rtype]!, + child: ElevatedButton( + onPressed: () { + switch (rtype) { + case RecipientType.public: + widget.setPublicFunction(); + case RecipientType.authUser: + widget.setAuthUsersFunction(); + case RecipientType.individual: + widget.setIndividualFunction(); + case RecipientType.group: + widget.setGroupFunction(); + case RecipientType.none: + return; + } + }, + child: Text(rtype.description), + ), + ), + ), + ), + ]; + } + + @override + Widget build(BuildContext context) { + // 20260109 jesscmoore Added capability for granters to share + // resources, as well as resource owners + return Container( + padding: const EdgeInsets.all(8.0), + height: 100, + child: Row( + children: widget.isExternalRes + + // jesscmoore 20260118: requires check grant/revoke to + // public/auth works on external resources + // av 20250526: + // Public and Authenticated recipient buttons are + // disabled currently because + // providing public or authenticated permissions to + // external resources is not yet implemented in + // [grantPermission()] function. + + // Recipient type buttons for resource granter + ? selectRecipientButtons( + allowedRecipientTypes: granterRecipientTypes, + ) + // Recipient type buttons for resource owner + : selectRecipientButtons( + allowedRecipientTypes: ownerRecipientTypes, + ), + ), + ); + } +} diff --git a/lib/src/solid/share_resource_button.dart b/lib/src/solid/share_resource_button.dart new file mode 100644 index 00000000..9b9304f5 --- /dev/null +++ b/lib/src/solid/share_resource_button.dart @@ -0,0 +1,220 @@ +/// A button for sharing a resource. +/// +// Time-stamp: +/// +/// Copyright (C) 2024-2025, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Jess Moore + +library; + +import 'package:flutter/material.dart'; + +import 'package:solidpod/src/solid/grant_permission_form.dart'; +import 'package:solidpod/src/solid/utils/alert.dart'; + +/// A [StatefulWidget] for sharing a resource, by either creating +/// an access permission for a new recipient or updating the access +/// permission of an existing recipient. +/// +/// Parameters: +/// - [resourceName] - The filename or file url of the resource. If [isExternalRes], it should be the url of the resource. +/// - [fileNameController] - The [TextEditingController] for the filename +/// field. +/// - [isExternalRes] - Boolean flag describing whether the resource +/// is externally owned. +/// - [ownerWebId] - WebId of the owner of the resource. Required if the resource is externally owned. +/// - [granterWebId] - WebId of the granter of the resource. Required if the resource is externally owned. +/// - [accessModeList] - List of access mode options to show. +/// - [recipientTypeList] - List of recipient type options to show. +/// - [isFile] - Boolean flag describing whether the resource is a file. If false, the resource is assumed to be a directory. +/// - [updatePermissionsFunction] is the function to be called to refresh the permission table. +/// +/// - [onPermissionGranted] - Callback function called when permissions are granted successfully. + +class ShareResourceButton extends StatefulWidget { + final TextEditingController fileNameController; + + /// String to assign the webId of the resource owner. + + final String ownerWebId; + + /// String to assign the external webId of the resource granter. + + final String granterWebId; + + /// The name of the file or directory that access is being granted for. + + final String? resourceName; + + final bool isExternalRes; + + /// A flag to determine whether the given resource is a file or not. + + final bool isFile; + + /// The list of access modes to be displayed. By default all four types of + /// access mode are listed. + + final List accessModeList; + + /// The list of types of recipients receiving permission to access the resource. By default all four + /// types of recipient are listed. + + final List recipientTypeList; + + /// Map of data files on a user's POD used to extract the + /// user's recipient list by the WebIdTextInputScreen. + /// If not provided, the WebIdTextInputScreen will read the + /// user's files in their app data folder on their Pod to + /// fetch the ACLs needed to derive the user's recipient list. + + final Map dataFilesMap; + + /// Function run to update permissions table + + final Function updatePermissionsFunction; + + /// Callback function called when permissions are granted successfully. + + final VoidCallback? onPermissionGranted; + + const ShareResourceButton({ + super.key, + required this.fileNameController, + required this.updatePermissionsFunction, + this.resourceName, + required this.ownerWebId, + required this.granterWebId, + this.accessModeList = const ['read', 'write', 'append', 'control'], + this.recipientTypeList = const ['public', 'indi', 'auth', 'group'], + required this.isExternalRes, + required this.isFile, + this.dataFilesMap = const {}, + this.onPermissionGranted, + }); + + @override + State createState() => _ShareResourceButtonState(); +} + +class _ShareResourceButtonState extends State { + /// Filename text controller + + late final TextEditingController _fileNameController; + + /// Owner WebId + + late final String _ownerWebId; + + /// Granter WebId + + late final String _granterWebId; + + /// Selected resource - assigned on Share Resource button press + + String _resourceName = ''; + + /// A flag to identify if the resource is a file or not + + bool isFile = true; + + /// Flag to track if permissions were granted successfully. + + bool permissionsGrantedSuccessfully = false; + + @override + void initState() { + super.initState(); + + _fileNameController = widget.fileNameController; + _ownerWebId = widget.ownerWebId; + _granterWebId = widget.granterWebId; + } + + @override + void dispose() { + _fileNameController.dispose(); // Dispose filename editing controller + super.dispose(); + } + + /// Mark permissions as granted successfully for callback tracking + Future _updatePermissionGrantedStatus() async { + setState(() => permissionsGrantedSuccessfully = true); + } + + /// Private function to call alert dialog in share resource button + /// context. This provides an alert dialog over the top of the + /// grant permission form dialog. + Future _alert(String msg) async => alert(context, msg); + + // Resource is a file if resource selected in GrantPermissionUi() + bool _getIsFile() => widget.resourceName != null ? widget.isFile : isFile; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8), + child: ElevatedButton.icon( + icon: const Icon( + Icons.share, + ), + onPressed: () async { + // Assign dataFile if null (first Grant press) + _resourceName = widget.resourceName ?? _fileNameController.text; + + if (_resourceName != '') { + // Display GrantPermissionForm dialog to enter + // recipient and access modes + await showDialog( + context: context, + builder: (BuildContext dialogContext) { + return GrantPermissionForm( + resourceName: _resourceName, + accessModeList: widget.accessModeList, + recipientTypeList: widget.recipientTypeList, + updatePermissionsFunction: widget.updatePermissionsFunction, + ownerWebId: _ownerWebId, + granterWebId: _granterWebId, + isExternalRes: widget.isExternalRes, + isFile: _getIsFile(), + dataFilesMap: widget.dataFilesMap, + updatePermissionGrantedFunction: + _updatePermissionGrantedStatus, + onPermissionGranted: widget.onPermissionGranted, + ); + }, + ); + } else { + await _alert( + 'Please select one or more recipients', + ); + } + }, + label: const Text('Share Resource'), + ), + ); + } +} diff --git a/lib/src/solid/show_selected_recipients.dart b/lib/src/solid/show_selected_recipients.dart new file mode 100644 index 00000000..0c2f87f3 --- /dev/null +++ b/lib/src/solid/show_selected_recipients.dart @@ -0,0 +1,101 @@ +/// A widget for showing selected recipients in the grant permission form. +/// +// Time-stamp: +/// +/// Copyright (C) 2024-2025, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Jess Moore, Anushka Vidanage + +library; + +import 'package:flutter/material.dart'; + +import 'package:solidpod/src/solid/constants/web_acl.dart'; + +/// A [StatelessWidget] for showing selected recipients in the +/// grant permission form. +/// +/// Parameters: +/// - [isExternalRes] - Boolean flag describing whether +/// the resource is externally owned. +/// - [recipientTypeList] - List of recipient type options to show. +/// - [setPublicFunction] - a function for setting recipients to +/// the public. +/// - [setAuthUsersFunction] - a function for setting recipients +/// to all authorised users. +/// - [setIndividualFunction] - a function for selecting an +/// individual recipient webId. +/// - [setGroupFunction] - a function for selecting a +/// group of webIds as recipients. +/// - [updateIndWebIdFunction] - a function to update the selected +/// individual webId. +/// - [updateGroupWebIdFunction] - a function to update the selected +/// group webId list and group name. + +class ShowSelectedRecipients extends StatelessWidget { + const ShowSelectedRecipients({ + super.key, + required this.selectedRecipientType, + required this.selectedRecipientDetails, + }); + + /// Selected recipient + + final RecipientType selectedRecipientType; + + /// Selected recipient details + + final String selectedRecipientDetails; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + const Text( + 'Recipient/s: ', + style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500), + ), + Flexible( + // Show recipients if selected + child: Text( + '${selectedRecipientType.type}${selectedRecipientDetails.isEmpty ? "" : " ($selectedRecipientDetails)"}', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + // 20251008 gjw Choose blue rather than + // orange which looks red. The red looks + // like it is an error. Blue is more + // neutral. + color: Colors.blueAccent, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/solid/utils/get_authoriser.dart b/lib/src/solid/utils/get_authoriser.dart new file mode 100644 index 00000000..7eacb1a1 --- /dev/null +++ b/lib/src/solid/utils/get_authoriser.dart @@ -0,0 +1,55 @@ +/// A button for sharing a resource. +/// +// Time-stamp: +/// +/// Copyright (C) 2024-2025, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Jess Moore, Anushka Vidanage + +library; + +import 'dart:core'; + +import 'package:solidpod/src/solid/utils/authdata_manager.dart'; + +/// Standardise retrieval of authoriser (ownerWebId or granterWebId) +/// for a resource in GrantPermissionUi +/// +/// Parameters: +/// - [isExternalRes] - flag describing whether [fileName] being shared is a file. +/// - [webId] - use webId if provided, else fetch user webId. + +Future getAuthoriser({ + bool isExternalRes = false, + String? webId, +}) async { + assert( + // Requires ownerWebId and granterWebId if resource + // is an externally owned. + isExternalRes == false || webId != null, + 'webId must be provided if isExternalRes == true', + ); + return isExternalRes ? webId! : await AuthDataManager.getWebId() as String; +} diff --git a/lib/src/widgets/file_permission_data_table.dart b/lib/src/widgets/file_permission_data_table.dart deleted file mode 100644 index 3c0dae5b..00000000 --- a/lib/src/widgets/file_permission_data_table.dart +++ /dev/null @@ -1,201 +0,0 @@ -/// A table displaying permission data for a given file. -/// -// Time-stamp: -/// -/// Copyright (C) 2024-2025, Software Innovation Institute, ANU. -/// -/// Licensed under the MIT License (the "License"). -/// -/// License: https://choosealicense.com/licenses/mit/. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -/// -/// -/// Authors: Anushka Vidanage, Jess Moore - -library; - -import 'package:flutter/material.dart' hide Key; - -import 'package:solidpod/src/solid/constants/common.dart'; -import 'package:solidpod/src/solid/constants/ui.dart'; -import 'package:solidpod/src/solid/constants/web_acl.dart'; -import 'package:solidpod/src/solid/revoke_permission.dart'; -import 'package:solidpod/src/solid/utils/snack_bar.dart'; - -/// Build the permission table widget. Function call requires the -/// following inputs. -/// -/// Parameters: -/// - [context] is the BuildContext from which this function is called. -/// - [permDataResource] is the name of the file or directory for which the -/// permission data is displayed -/// - [isFile] is the flag to define whether the resource is a file or not -/// - [ownerWebId] - is the web ID of the owner of the file. -/// - [granterWebId] - is the web ID of the granter of access to the file. This is usually the web ID of the user. -/// - [permDataMap] is the map of permission data for the [permDataResource] -/// - [parentWidget] is the widget to return to after an action Eg: deletion of a -/// permission -/// - [updatePermissionsFunction] is the function to be called to refresh the permission table. -/// -Widget buildPermDataTable({ - required BuildContext context, - required String permDataResource, - required bool isFile, - required Map permDataMap, - required String ownerWebId, - required String granterWebId, - required Widget parentWidget, - required Function updatePermissionsFunction, - bool isExternalRes = false, -}) { - DataColumn buildDataColumn(String title, String tooltip) { - return DataColumn( - label: Expanded( - child: Center( - child: Text( - title, - ), - ), - ), - tooltip: tooltip, - ); - } - - // Make wide permission table horizontally scrollable - // Shows when content exceeds display width - return DataTable( - columns: [ - buildDataColumn( - 'Receiver', - 'WebID of the permission recipient', - ), - buildDataColumn('Receiver type', 'Type of the receiver'), - buildDataColumn('Permissions', 'List of permissions given'), - buildDataColumn('Actions', 'Delete permission'), - ], - // index is the webId of each individual with access to the file - rows: permDataMap.keys.map((index) { - return DataRow( - cells: [ - DataCell( - Container( - padding: const EdgeInsets.fromLTRB(0, 10, 0, 0), - //width: cWidth, - child: Column( - children: [ - SelectableText( - (index.replaceAll('.ttl', '')) as String, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ], - ), - ), - ), - DataCell( - Text( - getRecipientType( - permDataMap[index][agentStr] as String, - index as String, - ).description, - ), - ), - DataCell( - Text( - (permDataMap[index][permStr] as List).join(', '), - ), - ), - // If recipient != owner, then show the delete permission button - if (ownerWebId != index) ...[ - DataCell( - IconButton( - icon: const Icon( - Icons.delete, - size: 24.0, - color: ActionColors.delete, - ), - onPressed: () { - showDialog( - context: context, - builder: (ctx) { - return AlertDialog( - title: const Text('Please Confirm'), - content: Text( - 'Are you sure you want to remove the [${(permDataMap[index][permStr] as List).join(', ')}] permission/s from ${index.replaceAll('.ttl', '')}?', - ), - actions: [ - // The "Yes" button - TextButton( - onPressed: () async { - await revokePermission( - fileName: permDataResource, - isFile: isFile, - permissionList: - permDataMap[index][permStr] as List, - recipientIndOrGroupWebId: index, - ownerWebId: ownerWebId, - granterWebId: granterWebId, - recipientType: getRecipientType( - permDataMap[index][agentStr] as String, - index, - ), - isExternalRes: isExternalRes, - ); - - if (ctx.mounted) { - Navigator.pop(ctx); - } - if (ctx.mounted) { - showSnackBar( - context, - 'Permission revoked successfully!', - ActionColors.success, - ); - } - await updatePermissionsFunction( - permDataResource, - isFile: isFile, - ); - }, - child: const Text('Yes'), - ), - TextButton( - onPressed: () { - // Close the dialog - Navigator.of(ctx).pop(); - }, - child: const Text('No'), - ), - ], - ); - }, - ); - }, - ), - ), - ] else ...[ - const DataCell( - Text(''), - ), - ], - ], - ); - }).toList(), - ); -}