bosskmk / pluto_grid

PlutoGrid is a dataGrid for flutter that can be controlled by the keyboard on desktop and web. Of course, it works well on Android and IOS.
https://pluto.weblaze.dev
MIT License
637 stars 291 forks source link

[Feature] set column filter value programmatically #939

Closed graemep-nz closed 9 months ago

graemep-nz commented 9 months ago

I need to be able to set the column filter value programmatically so that when someone types in a textbox outside the grid, the list of items shown in the grid changes to show only those that "contain" the value typed by the user. The row at the top of the grid that shows the current filters would update automatically as the user typed.

graemep-nz commented 9 months ago

Never mind, I figured out a way to do it


                                            PlutoGridChangeColumnFilterEvent(
                                              column: columns[0],
                                              filterType: columns[0].defaultFilter,
                                              filterValue: value,
                                              debounceMilliseconds:
                                                  gridStateManager!.configuration.columnFilter.debounceMilliseconds)); ```
froccawork commented 1 month ago

I also need the same thing but I didn't understand where to call the object you wrote above. Can you give me the complete example if possible?

Thanks a lot

graemep-nz commented 1 month ago

Here's the whole file


import 'package:flutter/material.dart';
import 'package:pluto_grid/pluto_grid.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:math';
import 'dart:convert';

//============================================================
// Bridge app application source file imports follow
//============================================================

import 'app_util.dart';
import 'widget_util.dart';
import 'firestore_data.dart';
import 'device_attributes.dart';
import 'choose_country.dart';
import 'choose_club.dart';
import 'user_login.dart';

// debug print
void _xprint(_){}  // no print
void _dbprint(String str) => print(str);   // session debug print

const String _info1 =
  'If your state/region or town/city do not appear in the lists above, '
  'please enter them manually below.';

// this widget requires a logged in user -  LoginManager.activeFirebaseUserUID
// UserProfileManager.activeUserProfile, LoginManager.isUserLoggedIn == true
class CreateBridgeClub extends StatefulWidget{
  String country;

  CreateBridgeClub({super.key, this.country = ""}) {
  }

  @override
  _CreateBridgeClub createState() => _CreateBridgeClub();

}

class _CreateBridgeClub extends State<CreateBridgeClub> {
  String                    countryName = "";
  String                    regionName = "";
  String                    cityName = "";
  String                    errorMessage = "";
  // String                    selectedClub = "";
  TextEditingController     clubNameTextController = TextEditingController();
  TextEditingController     clubStreetAddressTextController = TextEditingController();

  TextEditingController     clubCityTownTextController = TextEditingController();
  TextEditingController     clubStateTextController = TextEditingController();

  HandleDelayedTask         readCountryIndexDocTask = HandleDelayedTask.idle();
  //bool                      countryDocHasBeenRead = false;

  PlutoGridStateManager?    gridStateManager;
  List<PlutoRow>            plutoRows = [];

  // recentlyViewedClub  TODO
  @override
  void initState() {
    super.initState();
    countryName = widget.country;
    if (countryName != "") {
      readFirestoreCountryDoc(countryName);
    }
  }
  static const double totalMaxWindowWidth = 560;
  static List<PlutoColumn> columns = <PlutoColumn>[
    PlutoColumn(
      title: 'Club name',
      field: 'ClubName',
      type: PlutoColumnType.text(),
      width: 110,  // note - adjust totalMaxWindowWidth
      //readOnly: true,
    ),
    PlutoColumn(
      title: 'City/Town',
      field: 'CityTown',
      type: PlutoColumnType.text(),
      width: 110,
      //readOnly: true,
    ),
    PlutoColumn(
      title: 'Region/State',
      field: 'RegionState',
      type: PlutoColumnType.text(),
      width: 140,
      //readOnly: true,
    ),
    PlutoColumn(
      title: 'Street address',
      field: 'StreetAddress',
      type: PlutoColumnType.text(),
      width: 200,
      //readOnly: true,
    ),

  ];

  void readFirestoreCountryDoc(String country) async {
    //countryDocHasBeenRead = false;
    firestoreDb = FirebaseFirestore.instance;
    DocumentSnapshot<Map<String, dynamic>>? countryDoc;
    plutoRows = [];
    gridStateManager?.removeAllRows(notify:false);

    // TODO try local cache perhaps
    await readCountryIndexDocTask.runTask( () async => (countryDoc = await firestoreDb.doc(
                      FirestorePathnames.WWBridgeClubsIndexColl + "/" + country).get()), 6000);

    if (readCountryIndexDocTask.getTaskStatus == TaskStatus.finishOk && countryDoc != null) {
      if (countryDoc!.exists) {
        if (!AppData.bridgeClubIndexData.containsKey(country)) {
          AppData.bridgeClubIndexData[country] = BridgeClubsIndexData();
        }
        _dbprint(">>>>>>>>>>>>>>  read country doc");
        copyFromFirestoreCountryDoc(countryDoc!, AppData.bridgeClubIndexData[country]!);
        return;
      } else {
        //readCountryIndexDocTask.notifyListeners();  // this should be redundant, just making sure
      }
    }
    else {
      _dbprint("######Read country index doc failed 2");
      _dbprint(readCountryIndexDocTask.getTaskMessage);
      _dbprint(readCountryIndexDocTask.getTaskStatus.toString());
      //readCountryIndexDocTask.notifyListeners();  // this should be redundant, just making sure
    }
  }

  void copyFromFirestoreCountryDoc(DocumentSnapshot<Map<String, dynamic>> countryDoc,
                                              BridgeClubsIndexData indexData  ) {
    T getField<T>(Map<String, dynamic>? data, T res, String fieldName) {
      //print(res.runtimeType);
      if (data != null && data.containsKey(fieldName)) {
        return data[fieldName] ?? res;
      }
      return res;
    }
    if (countryDoc.data() == null) return;
    Map<String, dynamic> docData = countryDoc.data()!;
    indexData.formatCode = getField(docData, indexData.formatCode, "FormatCode");
    indexData.numberOfClubs = getField(docData, indexData.numberOfClubs, "NumberOfClubs");
    indexData.lastUpdatedFieldTimestamp
      = getField(docData, indexData.lastUpdatedFieldTimestamp, "lastUpdatedFieldTimestamp");

    // listofClubs is a json encoded list of strings that are as follows
    // <Firestore Document ID> <Club name> <region> <city/town> <street address>
    // e.g. ["0zgjwoCkEciypsSiLDM2","Christchurch","Canterbury","Christchurch", "15 Nova Pl]
    indexData.listOfClubs = [];
    indexData.clubData = [];
    plutoRows = [];

    if (docData.containsKey("ListOfClubs")) {
      int len = docData["ListOfClubs"].length;
      for (int k = 0; k < len; ++k) {
        var clubData = jsonDecode(docData["ListOfClubs"][k]);
        if (clubData.length >= 5) {
          indexData.listOfClubs.add(docData["ListOfClubs"][k]);
          indexData.clubData.add([for (var x in clubData) x]);
        }
      }
      // sort indexData.clubData
      indexData.clubData.sort( (list1, list2) {
        var str1 = list1[3] + list1[1];  // city | clubname
        var str2 = list2[3] + list2[1];
        return str1.compareTo(str2);
      });
      for (var clubData in indexData.clubData) {
        var aRow = PlutoRow(
          key: (ValueKey(clubData[1] + clubData[2] + clubData[3])),
          cells: {
            'ClubName': PlutoCell(value: clubData[1] as String),
            'RegionState': PlutoCell(value: clubData[2] as String),
            'CityTown': PlutoCell(value: clubData[3] as String),
            'StreetAddress': PlutoCell(value: clubData[4] as String),
          },
        );
        plutoRows.add(aRow);
      }
    }
    gridStateManager?.appendRows(plutoRows);
    //countryDocHasBeenRead = true;
    readCountryIndexDocTask.notifyListeners();

    //gridStateManager?.eventManager!.addEvent(event)
  }

  /// columnGroups that can group columns can be omitted.
  //final List<PlutoColumnGroup> columnGroups = [
  //  PlutoColumnGroup(title: 'Id', fields: ['id'], expandedColumn: true),
  //  PlutoColumnGroup(title: 'User information', fields: ['name', 'age']),
  //  PlutoColumnGroup(title: 'Status', children: [
  //    PlutoColumnGroup(title: 'A', fields: ['role'], expandedColumn: true),
  //    PlutoColumnGroup(title: 'Etc.', fields: ['joined', 'working_time']),
  //  ]),
  //];

  // https://github.com/bosskmk/pluto_grid/discussions/885

  void processCountrySelection(String country, String region, String city ) {
    _dbprint("cccccccccccccountry2  $country  $region  $city");
    if (country != countryName && country != "") {
      countryName = country;
      readFirestoreCountryDoc(country);
      clubStateTextController.text = "";
      regionName = "";
      clubCityTownTextController.text = "";
      cityName = "";
      return;
    }
    if (region != "") {
      clubStateTextController.text = region ?? "";
      regionName = region ?? "";
    }
    if (city != "") {
      clubCityTownTextController.text = city ?? "";
      cityName = city ?? "";
    }
  }

  void processCreateClubButton() {
    // TODO support non English  - or don't restrict characters
    //final validCharacters2 = RegExp(r"^[a-zA-Z0-9@#&*\ \-_\'\.\,\:\$\(\)\+\|\/\\]+$");
    final validCharacters = RegExp(r"^[a-zA-Z0-9@#&*%=?<> _'.,;:$()+|/\-\\]+$");
    errorMessage = "";

    cityName = clubCityTownTextController.text;
    regionName = clubStateTextController.text;

    if (countryName == countrySearchPlaceholderString || countryName == "") {
      errorMessage += "Please select a country";
    }
    if (regionName == stateSearchPlaceholderString || regionName == "") {
      errorMessage += "\nPlease select a region/state";
    }
    if (cityName == citySearchPlaceholderString || cityName == "") {
      errorMessage += "\nPlease select a town/city";
    }
    String str = clubNameTextController.text.trim();
    clubNameTextController.text = str;
    if (clubNameTextController.text.length < 6) {
      errorMessage += "\nPlease enter at least six characters for the club name";
    } else {
      if (!validCharacters.hasMatch(clubNameTextController.text)) {
        errorMessage += "\nError: illegal characters in club name";
      }
    }
    str = clubStreetAddressTextController.text.trim();
    clubStreetAddressTextController.text = str;

    if (clubStreetAddressTextController.text.length < 8) {
      errorMessage += "\nPlease enter at least 8 characters for the street address";
    } else {
      if (!validCharacters.hasMatch(clubStreetAddressTextController.text)) {
        errorMessage += "\nError: illegal characters in street address";
      }
    }
    if (errorMessage != "") {
      readCountryIndexDocTask.getNotifier.notifyListeners();
      return;
    }

    var clubName = clubNameTextController.text;
    for (var cdat in AppData.bridgeClubIndexData[countryName]!.clubData) {
      if (clubName == cdat[1] && regionName == cdat[2] && cityName == cdat[3]) {
        errorMessage = "Error : This club is already configured";
        readCountryIndexDocTask.getNotifier.notifyListeners();
        return;
      }
      // TODO if club name matches but not city, prompt user to confirm first
      // i.e. set a flag promptDuplicate
    }

    Map<String, dynamic> reqRecord = {
      "userUID" : LoginManager.activeFirebaseUserUID,
      "clubName" : clubNameTextController.text,
      "clubCountry" : countryName,
      "clubRegion" : regionName,
      "clubCity" : cityName,
      "clubStreetAddress" : clubStreetAddressTextController.text,
      "lastUpdatedFieldTimestamp" : FieldValue.serverTimestamp(),
    };

    firestoreDb.doc(FirestorePathnames.WWUsersColl + "/" + LoginManager.activeFirebaseUserUID
          + FirestorePathnames.UserDocsColl + "/${FirestoreDocNames.requestCreateClubDocName}")
      .update(reqRecord)
      .onError((e, _) => _dbprint("error writing document: $e"));

  }

  final ButtonStyle style = ElevatedButton.styleFrom(
    textStyle: const TextStyle(fontSize: 14),
    minimumSize: const Size(80, 44),
    //backgroundColor: Colors.orangeAccent, foregroundColor: Colors.deepPurple
  );

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: readCountryIndexDocTask.getNotifier,
      builder: (context2, value, child) {
        _dbprint("rebuilding >>>>>>>>>>>>>");

        return LayoutBuilder(
          builder: (BuildContext context, BoxConstraints constraints) {
            print(constraints);
            return SingleChildScrollView(
              child: Container(
                padding: const EdgeInsets.all(15),
                child: SizedBox(
                  width: DeviceAttributes.deviceIsPhone ? constraints.maxWidth - 30 :
                               min(totalMaxWindowWidth, constraints.maxWidth - 30),
                  child: Center(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        SizedBox(height: 10),
                        Text(" Create new Bridge club",
                          style: TextStyle(
                            fontWeight: FontWeight.bold, fontSize: 20, color: Colors.lightBlue)
                        ),
                        SizedBox(height: 22),
                        SizedBox(
                          width: DeviceAttributes.standardTextBoxWidthInDP,
                          child: FittedBox(
                            alignment: Alignment.topLeft,
                            //fit: BoxFit.,
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                SizedBox(
                                  width: DeviceAttributes.standardTextBoxWidthInDP,
                                  child: Row(       // TODO don't need Row
                                    children: [
                                      Expanded(
                                        child: ChooseLocation(key: ValueKey(4441), notifyCallback: processCountrySelection, showStates: true, showCities: true)
                                      ),
                                    ],
                                  ),
                                ),

                                SizedBox(height: 16),
                                SizedBox(child: Text(_info1,), width: DeviceAttributes.standardTextBoxWidthInDP),
                                SizedBox(height: 20),

                                SizedBox(
                                  height: 40,
                                  width: DeviceAttributes.standardTextBoxWidthInDP,
                                  child: Row(
                                    children: [
                                      SizedBox(
                                        width: DeviceAttributes.standardTextBoxWidthInDP/2 - 6,
                                        child: TextField(
                                          controller: clubStateTextController,
                                          clipBehavior: Clip.none,
                                          decoration: InputDecoration(
                                            border: OutlineInputBorder(
                                              borderSide: BorderSide(width: 1, color: Colors.lime),
                                            ),
                                            labelText: stateSearchPlaceholderString,
                                            filled: true,
                                            fillColor: Colors.white,
                                            enabledBorder: OutlineInputBorder(
                                              borderSide: BorderSide(width: 1, color: Colors.black12),
                                            ),
                                          ),
                                        ),
                                      ),
                                      SizedBox(width:12),
                                      SizedBox(
                                        width: DeviceAttributes.standardTextBoxWidthInDP/2 - 6,
                                        child: TextField(
                                          controller: clubCityTownTextController,
                                          onChanged: (String value) {
                                            gridStateManager!.eventManager!.addEvent(
                                                  PlutoGridChangeColumnFilterEvent(
                                                    column: columns[1],
                                                    filterType: columns[1].defaultFilter,
                                                    filterValue: value,
                                                    debounceMilliseconds:
                                                        gridStateManager!.configuration.columnFilter.debounceMilliseconds));
                                          },
                                          clipBehavior: Clip.none,
                                          decoration: InputDecoration(
                                            border: OutlineInputBorder(
                                              borderSide: BorderSide(width: 1, color: Colors.lime),
                                            ),
                                            labelText: citySearchPlaceholderString,
                                            filled: true,
                                            fillColor: Colors.white,
                                            enabledBorder: OutlineInputBorder(
                                              borderSide: BorderSide(width: 1, color: Colors.black12),
                                            ),
                                          ),
                                        ),
                                      ),
                                    ],
                                  ),
                                ),

                                SizedBox(height: 15),
                                SizedBox(
                                  height: 40,
                                  width: DeviceAttributes.standardTextBoxWidthInDP,
                                  child: TextField(
                                    controller: clubNameTextController,
                                    onChanged: (String value) {
                                      gridStateManager!.eventManager!.addEvent(
                                            PlutoGridChangeColumnFilterEvent(
                                              column: columns[0],
                                              filterType: columns[0].defaultFilter,
                                              filterValue: value,
                                              debounceMilliseconds:
                                                  gridStateManager!.configuration.columnFilter.debounceMilliseconds));
                                    },
                                    clipBehavior: Clip.none,
                                    decoration: InputDecoration(
                                      border: OutlineInputBorder(
                                        borderSide: BorderSide(width: 1, color: Colors.lime),
                                      ),
                                      labelText: 'Club name',
                                      filled: true,
                                      fillColor: Colors.white,
                                      enabledBorder: OutlineInputBorder(
                                        borderSide: BorderSide(width: 1, color: Colors.black12),
                                      ),
                                    ),
                                  ),
                                ),
                                SizedBox(height: 12),
                                SizedBox(
                                  height: 40,
                                  width: DeviceAttributes.standardTextBoxWidthInDP,
                                  child: TextField(
                                    controller: clubStreetAddressTextController,
                                    clipBehavior: Clip.none,
                                    decoration: InputDecoration(
                                      border: OutlineInputBorder(
                                        borderSide: BorderSide(width: 1, color: Colors.lime),
                                      ),
                                      labelText: 'Street address',
                                      filled: true,
                                      fillColor: Colors.white,
                                      enabledBorder: OutlineInputBorder(
                                        borderSide: BorderSide(width: 1, color: Colors.black12),
                                      ),
                                    ),
                                  ),
                                ),

                                SizedBox(height: 10),
                                if (errorMessage != "") Text(errorMessage,style: myStyles.errorTextStyle),
                                SizedBox(height: 10),
                                SizedBox(
                                  width:190,
                                  child: Row(
                                    children: [
                                      ElevatedButton(
                                        style: style,
                                        onPressed: () {
                                          processCreateClubButton();
                                        },
                                        child: const Text('Create\nclub'),
                                      ),
                                      SizedBox(width: 20),
                                      ElevatedButton(
                                        style: style,
                                        onPressed: () {
                                          selectChooseBridgeClubWidget();
                                        },
                                        child: const Text('Cancel'),
                                      ),
                                    ]
                                  ),
                                ),
                              ],
                            ),
                          ),
                        ),
                        SizedBox(height: 22),
                        SizedBox(child: Text('Clubs already in the database for your country'), width: DeviceAttributes.standardTextBoxWidthInDP),
                        SizedBox(height: 12),

                        SizedBox(
                          height:  400,  // min(constraints.maxHeight - 200, 900),
                          //padding: const EdgeInsets.all(15),
                          //width: 400,
                          /*
                          child: FittedBox(
                            fit: BoxFit.contain,
                            clipBehavior: Clip.hardEdge,
                            child: Container(
                              width: 800,
                              height: 700,
                              child: Text("HHHHHHHUUUUuuuuuuKKKKK123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"),
                              color: Colors.redAccent,
                            ),
                          )
                          */

                          child: PlutoGrid(
                            columns: columns,
                            rows: [], //plutoRows,
                            //columnGroups: columnGroups,
                            onLoaded: (PlutoGridOnLoadedEvent event) {
                              gridStateManager = event.stateManager;
                              gridStateManager?.setShowColumnFilter(true);
                              _dbprint("grid loaded >>>>>>>>>>>>>>>>>>>>>>>");
                            },
                            onChanged: (PlutoGridOnChangedEvent event) {
                              print(event);
                            },
                            mode: PlutoGridMode.selectWithOneTap,  // this makes the table read only
                            // onSelected: (PlutoGridOnSelectedEvent event) {
                            //   selectedClub = plutoRows[event.rowIdx!].cells['ClubName']!.value;
                            //   chosenClubTextController.text = selectedClub;
                            //   //print(event);
                            //   //print(event.rowIdx);
                            // },
                            configuration: const PlutoGridConfiguration(),
                            createFooter: (stateManager) {
                              stateManager.setPageSize(8, notify: false); // default 40
                              return PlutoPagination(stateManager);
                            },
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            );
          }
        );
      }
    );
  }
}
froccawork commented 1 month ago

Perfect! Thanks a lot! In your experience, would it be possible to search multiple columns from a single textField? for example you write:

onChanged: (String value) { gridStateManager!.eventManager!.addEvent( PlutoGridChangeColumnFilterEvent( column: columns[1], filterType: columns[1].defaultFilter, filterValue: value, debounceMilliseconds: gridStateManager!.configuration.columnFilter.debounceMilliseconds)); },

Would it be possible to search across multiple columns instead of putting columns[1]?

graemep-nz commented 1 month ago

I don't have any experience of that but I don't see why why it wouldn't work. Just call addEvent for each column and see if it works. I'm not actually using plutoGrid any more - I switched to using syncfusion datagrid.

froccawork commented 1 month ago

I was also thinking of using syncfusion datagrid but I saw that you have to pay, correct?

graemep-nz commented 1 month ago

Only if your gross revenue exceeds one million dollars or something, you need to have 5 or less developers and less than ten employees https://www.syncfusion.com/products/communitylicense

froccawork commented 1 month ago

then yes we should pay... how is your experience with syncfusion datagrid?

syncfusion datagrid vs pluto_grid?

graemep-nz commented 1 month ago

There's a bunch of examples you can try here. https://flutter.syncfusion.com/ I swapped because pluto-grid wasn't being maintained plus there was a slight glitch with the mouse cursor when it was over a filter icon. pluto-grid seemed like high quality, possibly better software than syncfusion, I don't really know which has the most capability. Probably syncfusion will have better support even for the free license I think.

graemep-nz commented 1 month ago

also probably this is the one to use now https://pub.dev/packages/pluto_grid_plus

froccawork commented 1 month ago

I'm already using that