Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Flutter on TabKey Focus not Moving Properly #2153

Open
abdulrehmananwar opened this issue Oct 29, 2024 · 15 comments
Open

Flutter on TabKey Focus not Moving Properly #2153

abdulrehmananwar opened this issue Oct 29, 2024 · 15 comments
Labels
data grid Data grid component waiting for customer response Cannot make further progress until the customer responds.

Comments

@abdulrehmananwar
Copy link

Bug description

i have write a logic on buildEditWidget event after that focus travel on tab key not working properly . my code is as below.
if (column.columnName == 'Quantity2') {
return null;
}

Steps to reproduce

@OverRide
Widget? buildEditWidget(DataGridRow dataGridRow,
RowColumnIndex rowColumnIndex, GridColumn column, CellSubmit submitCell) {
// Check if the column is 'Quantity2' and return null if so
if (column.columnName == 'Quantity2') {
return null;
}

// Fetch the current cell value to display
final String displayText = dataGridRow
    .getCells()
    .firstWhereOrNull(
        (DataGridCell dataGridCell) => dataGridCell.columnName == column.columnName)
    ?.value
    ?.toString() ??
    '';
newCellValue = _isPasting.value ? null : displayText;

// Create a TextEditingController to manage text input
final TextEditingController editingController = TextEditingController(
  text: displayText,
);

return Focus(
  onKeyEvent: (node, keyEvent) {
    if (keyEvent.logicalKey == LogicalKeyboardKey.keyV &&
        HardwareKeyboard.instance.isControlPressed) {
      handlePasteAndNavigate(rowColumnIndex.rowIndex, rowColumnIndex.columnIndex);
      return KeyEventResult.handled;
    }
    return KeyEventResult.ignored;
  },
  child: TextField(
    autofocus: true,
    controller: editingController,
    decoration: const InputDecoration(
      contentPadding: EdgeInsets.fromLTRB(0, 0, 0, 8.0),
    ),
    onChanged: (String value) {
      if (value.isNotEmpty) {
        newCellValue = value;
      }
    },
    onSubmitted: (String value) {
      // Submit the cell's value
      submitCell();
    },
  ),
);

}

Code sample

import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.dart';
import 'package:flutter/services.dart';
import 'package:collection/collection.dart';

class CustomSelectionManager extends RowSelectionManager {
CustomSelectionManager({
required this.isDialogOpen,
required this.dataGridController,
required this.dataGridSource,
required this.context,
required this.lastColumnIndex,
});

final bool isDialogOpen;
final DataGridController dataGridController;
final DataGridSource dataGridSource;
final int lastColumnIndex;
final BuildContext context;

@OverRide
Future handleKeyEvent(KeyEvent keyEvent) async {
if (keyEvent.logicalKey == LogicalKeyboardKey.tab ||
keyEvent.logicalKey == LogicalKeyboardKey.enter) {
if (!isDialogOpen) {
await super.handleKeyEvent(keyEvent).then(
(value) =>
WidgetsBinding.instance.addPostFrameCallback((_) async {
await dataGridController
.beginEdit(dataGridController.currentCell);
}),
);
}
}
}
}

class GridEntryDataSource extends DataGridSource {
GridEntryDataSource(this.dataGridController, this.fieldNames) {
_initializeDataList();
updateDataGridRows();
}

DataGridController dataGridController;
final List fieldNames;
List dataGridRows = [];
List<Map<String, String>> dataList = [];
// Change _isPasting to ValueNotifier
final ValueNotifier _isPasting = ValueNotifier(false);

dynamic newCellValue;

void _initializeDataList() {
dataList.add({for (var v in fieldNames) v: ''});
}

@OverRide
List get rows => dataGridRows;

void updateDataGridRows() {
dataGridRows =
dataList.map((data) => _createDataRow(data)).toList();
notifyListeners();
}

DataGridRow _createDataRow(Map<String, String> data) {
return DataGridRow(
cells: fieldNames
.map((field) =>
DataGridCell(columnName: field, value: data[field]))
.toList(),
);
}

Future handlePasteAndNavigate(
int startRowIndex, int startColumnIndex) async {
if (_isPasting.value) return;
_isPasting.value = true;
final clipboardData = await Clipboard.getData('text/plain');

if (clipboardData != null) {
  List<String> rows = clipboardData.text!.split('\n');

  for (int i = 0; i < rows.length - 1; i++) {
    List<String> columns = rows[i].split('\t');

    // Ensure we have enough rows in the dataList.
    while (dataList.length <= startRowIndex + i) {
      dataList.add({for (var v in fieldNames) v: ''});
    }

    for (int j = 0; j < columns.length; j++) {
      if (startColumnIndex + j < fieldNames.length) {
        newCellValue = null;
        int rowIndex = startRowIndex + i;
        int colIndex = startColumnIndex + j;

        // Enter edit mode for the target cell
        RowColumnIndex rowColumnIndex = RowColumnIndex(rowIndex, colIndex);
        // Add a minimal delay before entering edit mode
        await Future.delayed(const Duration(milliseconds: 100));
        await dataGridController.beginEdit(rowColumnIndex);

        // Ensure that cell submission is processed after a slight delay
        await Future.delayed(const Duration(milliseconds: 100));
        _onCellSubmitted(
            dataGridRows[rowIndex], fieldNames[colIndex], columns[j]);

        // Move to the edited cell and end edit mode
        dataGridController.moveCurrentCellTo(rowColumnIndex);
        dataGridController.endEdit();
      }
    }
  }
}
_isPasting.value = false;

}

void resetGrid() {
dataList.clear();
_initializeDataList();
updateDataGridRows();
}

Future _onCellSubmitted(
DataGridRow row, String columnName, String value) async {
final rowIndex = dataGridController.currentCell.rowIndex;

// Validate and assign value to the correct dataList entry.
if (rowIndex >= 0 && rowIndex < dataList.length) {
  dataList[rowIndex][columnName] = value;

  // Add a new row if the last row is filled.
  if (rowIndex == dataList.length - 1 &&
      dataList[rowIndex].values.any((v) => v.isNotEmpty)) {
    dataList.add({for (var v in fieldNames) v: ''});
    if (dataGridController.currentCell.columnIndex == 0) {
      final lastData = dataList.last;
      dataGridRows.add(_createDataRow(lastData));
      notifyListeners();
    }
  }
} else {
  print("Invalid row index during submission: $rowIndex");
}

}

@OverRide
Future onCellSubmit(DataGridRow dataGridRow,
RowColumnIndex rowColumnIndex, GridColumn column) async {
// Get the current row index from the data grid controller.
final currentRowIndex = dataGridController.currentCell.rowIndex;

// Check if the current row index is valid.
if (currentRowIndex < 0 || currentRowIndex >= dataList.length) {
  print("Invalid row index: $currentRowIndex");
  return;
}

// Retrieve the field name based on the current column index.
var names = fieldNames[dataGridController.currentCell.columnIndex];

dynamic value = dataList[currentRowIndex][names];

if (value == newCellValue) {
  return;
}

// Update the value if the new cell value is not null or empty.
if (newCellValue != null &&
    newCellValue.isNotEmpty &&
    value != newCellValue) {
  value = newCellValue;
  dataList[currentRowIndex][names] = value;
}

// If the updated value is valid, update the corresponding cell in the data grid row.
if (value != null && value.isNotEmpty) {
  if (column.columnName == names) {
    dataGridRows[currentRowIndex]
        .getCells()[dataGridController.currentCell.columnIndex] =
        DataGridCell<String>(columnName: names, value: value);
  }
}

}

@OverRide
Future canSubmitCell(DataGridRow dataGridRow,
RowColumnIndex rowColumnIndex, GridColumn column) async {
return Future.value(true);
}

@OverRide
DataGridRowAdapter buildRow(DataGridRow row) {
return DataGridRowAdapter(
cells: row.getCells().map((cell) {
return Container(
alignment: Alignment.center,
child: Text(cell.value?.toString() ?? ''),
);
}).toList(),
);
}

@OverRide
Widget? buildEditWidget(DataGridRow dataGridRow,
RowColumnIndex rowColumnIndex, GridColumn column, CellSubmit submitCell) {
// Check if the column is 'Quantity2' and return null if so
if (column.columnName == 'Quantity2') {
return null;
}

// Fetch the current cell value to display
final String displayText = dataGridRow
    .getCells()
    .firstWhereOrNull(
        (DataGridCell dataGridCell) => dataGridCell.columnName == column.columnName)
    ?.value
    ?.toString() ??
    '';
newCellValue = _isPasting.value ? null : displayText;

// Create a TextEditingController to manage text input
final TextEditingController editingController = TextEditingController(
  text: displayText,
);

return Focus(
  onKeyEvent: (node, keyEvent) {
    if (keyEvent.logicalKey == LogicalKeyboardKey.keyV &&
        HardwareKeyboard.instance.isControlPressed) {
      handlePasteAndNavigate(rowColumnIndex.rowIndex, rowColumnIndex.columnIndex);
      return KeyEventResult.handled;
    }
    return KeyEventResult.ignored;
  },
  child: TextField(
    autofocus: true,
    controller: editingController,
    decoration: const InputDecoration(
      contentPadding: EdgeInsets.fromLTRB(0, 0, 0, 8.0),
    ),
    onChanged: (String value) {
      if (value.isNotEmpty) {
        newCellValue = value;
      }
    },
    onSubmitted: (String value) {
      // Submit the cell's value
      submitCell();
    },
  ),
);

}

}

class DataGridExample extends StatefulWidget {
const DataGridExample({super.key});

@OverRide
// ignore: library_private_types_in_public_api
_DataGridExampleState createState() => _DataGridExampleState();
}

class _DataGridExampleState extends State {
List fieldNames = ['ItemName', 'Quantity', 'Quantity2', 'Price', 'Amount'];
late DataGridController dataGridController;
late GridEntryDataSource dataSource;

String selectedOption = 'AgeFields';

@OverRide
void initState() {
super.initState();
dataGridController = DataGridController();
dataSource = GridEntryDataSource(dataGridController, fieldNames);
}

void changeColumns(String option) {
setState(() {
selectedOption = option;
fieldNames.clear();
dataGridController = DataGridController();
dataSource = GridEntryDataSource(dataGridController, fieldNames);
});
}

@OverRide
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Editable DataGrid Example'),
actions: [
ValueListenableBuilder(
valueListenable: dataSource._isPasting,
builder: (context, isPasting, child) {
return IgnorePointer(
ignoring: isPasting,
child: IconButton(
icon: const Icon(Icons.paste),
onPressed: () async {
final focusedCell = dataSource.rows.first;
int rowIndex = dataSource.rows.indexOf(focusedCell);
await dataSource.handlePasteAndNavigate(rowIndex, 0);
},
),
);
},
),
ValueListenableBuilder(
valueListenable: dataSource.isPasting,
builder: (context, isPasting, child) {
return IgnorePointer(
ignoring: isPasting,
child: IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
dataGridController.endEdit();
WidgetsBinding.instance.addPostFrameCallback((
) {
dataSource.resetGrid();
});
},
),
);
},
),
],
),
body: Column(
children: [
Expanded(
child: SfDataGrid(
allowColumnsResizing: true,
selectionMode: SelectionMode.single,
navigationMode: GridNavigationMode.cell,
editingGestureType: EditingGestureType.tap,
allowEditing: true,
source: dataSource,
controller: dataGridController,
selectionManager: CustomSelectionManager(
isDialogOpen: false,
dataGridController: dataGridController,
dataGridSource: dataSource,
context: context,
lastColumnIndex: fieldNames.length - 1,
),
columns: fieldNames.map((fieldName) {
return GridColumn(
columnName: fieldName,
label: Container(
alignment: Alignment.center,
child: Text(fieldName,
style: const TextStyle(fontWeight: FontWeight.bold)),
),
);
}).toList(),
),
),
],
),
);
}
}

void main() {
runApp(const MaterialApp(
home: DataGridExample(),
debugShowCheckedModeBanner: false,
));
}

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Stack Traces

Stack Traces
[Add the Stack Traces here]

On which target platforms have you observed this bug?

Windows

Flutter Doctor output

Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 3.24.4, on Microsoft Windows [Version 10.0.19045.5073], locale en-US)
[√] Windows Version (Installed version of Windows is version 10 or higher)
[√] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
[√] Chrome - develop for the web
[√] Visual Studio - develop Windows apps (Visual Studio Professional 2022 17.10.4)
[√] Android Studio (version 2024.2)
[√] Connected device (3 available)
[√] Network resources

@abineshPalanisamy
Copy link

Hi @abdulrehmananwar ,

Based on the details provided, in the buildEditWidget, you have returned null for the Quantity2 column. The buildEditWidget is used to obtain the TextField widget when a cell is moved into edit mode. Therefore, while the cell was intended to be moved into edit mode, you have restricted the addition of the TextField widget by returning null. As a result, the cells in the Quantity2 column did not enter edit mode, even though focus switched to it. We believe that the cells in the Quantity2 column not entering edit mode is the issue you encountered.

To better assist you with your query, could you clarify why you are restricting the addition of the TextField widget for the cells in the Quantity2 column when focus switches?. Additionally, please provide a video recording demonstrating the issue for better clarity. Please provide relevant details that may help us better understand your request.

Regards,
Abinesh P

@ashok-kuvaraja ashok-kuvaraja added data grid Data grid component waiting for customer response Cannot make further progress until the customer responds. labels Oct 30, 2024
@abdulrehmananwar
Copy link
Author

I am currently facing three major issues with focus management when handling the CanCellSubmit event, particularly when opening a dialog to filter search results based on user input.

TextField Not Focusable: When the dialog opens, the TextField within it is not receiving focus. Even when clicked, it does not gain focus, which prevents the user from entering input.

Dialog Reopening Upon Dismissal: If the dialog is dismissed without selecting any value, it unexpectedly reopens immediately. On the second dismissal, it closes properly. The dialog should only reopen if the user presses the Tab key or clicks elsewhere, not automatically upon dismissal.

Focus Management in Price Column: I have a price column where user input is not required, so I do not return a TextField in the widget builder. However, this causes issues with focus traversal when using the Tab key, as the focus does not move as expected.

i have shared you sample Code and video please respond on it Thanks.

https://github.com/user-attachments/assets/0f5c454f-480d-4a2c-a059-c5451f4b018f
GridIssues.zip

@abineshPalanisamy
Copy link

Hi @abdulrehmananwar

 

Query Response
Dialog Reopening Upon Dismissal: If the dialog is dismissed without selecting any value, it unexpectedly reopens immediately. On the second dismissal, it closes properly. The dialog should only reopen if the user presses the Tab key or clicks elsewhere, not automatically upon dismissal. I have adjusted the sample you provided, and we have resolved the issue with opening and closing the dialog box. In the handleKeyEvent method of the RowSelectionManager, we need to set the dialog box flag variable to true after handling the key event and then to false when appropriate. This will rectify the issue with the dialog box opening and closing.
Focus Management in Price Column: I have a price column where user input is not required, so I do not return a TextField in the widget builder. However, this causes issues with focus traversal when using the Tab key, as the focus does not move as expected. After rectifying the issue, the dialog box now opens and closes properly. We are unable to reproduce the focus traversal issue when using the Tab key on our side. Based on the video reference, we attempted to replicate the issue, but it seems to have been resolved.
TextField Not Focusable: When the dialog opens, the TextField within it is not receiving focus. Even when clicked, it does not gain focus, which prevents the user from entering input. Based on the details provided, you are triggering the showDialog in the canSubmitCell method. The DataGridSource.canSubmitCell is called before the cell finishes editing. If you want to prevent the cell from ending its editing state, you can return false. In this scenario, the DataGrid cell remains in edit mode with the TextField active, so the focus is maintained solely on the DataGrid. When the dialog box opens, the focus stays on the DataGrid because the cell is still in edit mode.   Instead of triggering the dialog box in canSubmitCell, you can directly invoke showDialog in the buildEditWidget method for the specific cell based on the columnName, rather than loading the TextField widget.  

If there are any misunderstandings regarding your requirements, or if your needs differ, please provide specific and clear details about why you are using showDialog in canSubmitCell. What is the core reason for including the dialog? This additional information will help us thoroughly address your request and provide an appropriate solution.


We have included a modified sample for your reference. Please review it for further details

 

Regards,

Abinesh P

@abdulrehmananwar
Copy link
Author

Point Number 3 is still pending. You suggested invoking showDialog in the buildEditWidget method for the specific cell based on the columnName, instead of directly loading the TextField widget.

However, the TextField widget is necessary due to the following requirements:

Unfocus Behavior: When the TextField loses focus, we need to validate the input:
If the input is not empty and differs from the original value:
First, check if the input matches an item in the dialog's list:
If the match count is 1, accept the input without opening the dialog.
If the match count is greater than 1, open the dialog for the user to filter further.
Second, if the user closes the dialog without selecting an item, submission will remain disabled until a valid value is selected or the input is cleared.

chrome_hyRwyuPra9.mp4

@abdulrehmananwar
Copy link
Author

Issue Number 2 still not resolved. on buildEditWidget simply return null for all cells. and press tab key it not moving. properly.

@abineshPalanisamy
Copy link

Hi @abdulrehmananwar ,

TextField Not Focusable:

We have modified the sample according to your requirements. Instead of placing the DialogBox in the canSubmitCell method, we have now added it in the handleKeyEvent method. Additionally, we called DataGridController.endEdit to remove the focus from the TextField and allow focus to be set on the TextField within the dialog box. We have included a sample for your reference. Please review it for further details.

Press tab key it not moving Properly:

We have checked on our end, and the Tab key is working properly in the DataGrid. Tab key behavior moves the focus from the current cell to the next active cell. In your case, for the price column, you have a buildEditWidget that simply returns null for all cells, so the cell does not move into edit mode. When you press Tab, the focus switches to the price cell, but the cell doesn't enter edit mode.

We currently do not have direct support for copying and pasting cell content in the DataGrid. You have achieved this behavior through a custom implementation in the sample, but this approach may cause various issues in several use cases.

At present, the DataGrid does not support copying and pasting cell content. However, we have already considered your request as a feature for future releases. During the planning phase of each release cycle, we review all open feature requests and prioritize them based on factors such as product vision, technological feasibility, and customer demand. We appreciate your patience and understanding as we work towards implementing this feature. You can follow up with the feedback link provided for further updates.

Feedback link: 37701

Regards,
Abinesh P

@abdulrehmananwar
Copy link
Author

abdulrehmananwar commented Nov 7, 2024

1-You can remove Copypaste Feature from this code its not required.
2-Currently, the dialog only opens when the Tab key is pressed. However, I need it to open when the TextField loses focus, provided that the TextField has a value and is not empty
2-The dialog should open only when the user enters a value into the TextField.
3-If the user closes the dialog without making a selection, the cell should automatically re-enter edit mode.
4-On losing focus, the dialog should reopen until the user either selects a value or clears the TextField.
5-Currently Dialog Opening only on TabKey butt i need this to open on textfield Unfocus if TextField value input and not empty
6- Focus issue persists when the widget is null. I’m not referring to edit mode—just aiming for smooth focus movement, which isn’t happening as expected. Please refer to the attached video for clarification.

sboapp_FFcPdbPOsF.mp4

@abineshPalanisamy
Copy link

Hi @abdulrehmananwar

Based on the requirements, we have modified the sample to meet your needs. To open the dialog box when the TextField loses focus, we added the dialog box in the onFocusChange callback of the Focus widget. Additionally, we have addressed other focus management issues. Please refer to the sample for more details, and we have attached a video reference as well.

Video reference :

Video.reference.mp4

Regards,
Abinesh P

@abdulrehmananwar
Copy link
Author

abdulrehmananwar commented Nov 11, 2024

There are still several issues persisting:

1-When I press the Tab key without any input in the search text field, it doesn’t move to the next cell as expected.
2-The search dialog only opens for the first row but should open for each row.
3-When the search dialog opens, it currently clears the text in the background text field. However, this text should remain if the user cancels the dialog. Additionally, focus should stay on the cell until the user either selects a valid value from the dialog or clears the text field.
4-As shown in the video, sometimes when I clear the search text field and press the Tab key, the dialog still opens, and the Tab key does not behave as expected.
5- As demonstrated, when I click on the Quantity column, input a quantity, the value becomes invisible.

sboapp_WH3vuRstGP.mp4

@abineshPalanisamy
Copy link

Hi @abdulrehmananwar

We have addressed some of the issues and use cases in the sample. We have ensured that the specific use cases you mentioned are included, and we’ve provided a sample and video for your reference.

If you encounter any other issues, please review the entire sample and provide all related use cases. This will help us verify all expected behaviors and ensure that any remaining issues are resolved.

Video :

video.mp4

Regards,
Abinesh P

@abdulrehmananwar
Copy link
Author

abdulrehmananwar commented Nov 12, 2024

1-Tab Key Navigation: Pressing Tab in the Price column causes a slight lag, making the transition feel unsmooth. Additionally, Shift+Tab does not function properly in the Price column.
2- I need an additional functionality where, when the user unfocuses or presses the Tab key, the system checks if the entered value (e.g., '1') exists in the filter list. If the value occurs exactly once in the list, it should be automatically selected in the grid without opening the dialog box. If the value is not found or appears more than once, the dialog box should open for manual selection
Remember, Step 2 should always involve checking data asynchronously with await. For example, it will call an API to fetch a list and filter the data. The await functionality works fine when the user presses the Tab key, but it doesn't work correctly when the user unfocuses the cell by clicking with the mouse

@abdulrehmananwar
Copy link
Author

abdulrehmananwar commented Nov 13, 2024

Issue 1 was resolved by defining the CustomSelectionManager in initState and passing it to the build widget, rather than defining it directly within the build widget. butt i dont want to create into initState cause i am rebuilding Gridview. so i cannot define it on initState .

@abineshPalanisamy
Copy link

abineshPalanisamy commented Nov 13, 2024

Hi @abdulrehmananwar ,

As per your requirements, we have modified the sample. Now, the dialog box will open when the entered value does not match any items in the list. In showSelectionDialog, we added logic to check for value matches. In the provided sample, we are currently using a local list to achieve this behavior. To assist you further, could you please provide more details about your requirements, specifically regarding fetching the list of records from an API and applying filtering?

Regarding the Shift+Tab issue: all cells except for the ItemName column handle Shift key navigation and the Tab key properly. For the ItemName column, when the dialog box opens, it waits for a value to be chosen. However, this wait causes the framework to recognize the Shift key as inactive, preventing the DataGrid from detecting the Shift key press and thus disrupting navigation for this specific column. Unfortunately, this is a limitation on the DataGrid side that we cannot directly handle.

If you encounter any additional issues, please review the entire sample and provide all use cases. We will test each use case to confirm that expected behaviors function properly without impacting existing features, and we’ll address any remaining issues to ensure they are fully resolved.

  • Sample :

SfDataGrid.zip

Video:

Video.sample.mp4

Regards,
Abinesh P

@abdulrehmananwar
Copy link
Author

Issue 1: Initially, the CustomSelectionManager was moved to initState and passed to the build widget instead of being defined directly within the build widget. However, I would prefer not to define it in initState because I am rebuilding the GridView, so placing it in initState does not meet the requirements.

Issue 2: As mentioned, the list view data comes from the API on each unfocus event. Currently, you have hardcoded this list as List searchRecord = ["1", "2", "3"];, which is fine for now. Also, please call await Future.delayed(Duration(seconds: 10)); before returning the list. This setup works as expected when pressing the Tab key, but if a user enters input and then clicks on another cell, it doesn't trigger as expected."

@abineshPalanisamy
Copy link

Hi @abdulrehmananwar ,

Issue 1: We are unable to replicate the reported issue on our end. To assist you better, could you kindly provide more details regarding your requirements? Additionally, please share a video recording demonstrating the issue for better clarity.

Issue 2: We are unable to achieve the desired behavior on our end. When switching the focus of the cell via a mouse click and waiting for the list, the onTap event of the cell, wrapped in a GestureDetector, gets triggered by the framework at the source level. The framework's onTap event doesn't account for the delay we have introduced, which causes the issue and prevents the required behavior from being achieved.

Regards,
Abinesh P

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
data grid Data grid component waiting for customer response Cannot make further progress until the customer responds.
Projects
None yet
Development

No branches or pull requests

3 participants