Line data Source code
1 : part of apptive_grid_form_widgets;
2 :
3 : /// FormComponent Widget to display a [CrossReferenceFormComponent]
4 : class CrossReferenceFormWidget extends StatefulWidget {
5 : /// Creates a [Checkbox] to display a boolean value contained in [component]
6 2 : const CrossReferenceFormWidget({
7 : Key? key,
8 : required this.component,
9 2 : }) : super(key: key);
10 :
11 : /// Component this Widget should reflect
12 : final CrossReferenceFormComponent component;
13 :
14 1 : @override
15 : _CrossReferenceFormWidgetState createState() =>
16 1 : _CrossReferenceFormWidgetState();
17 : }
18 :
19 : class _CrossReferenceFormWidgetState extends State<CrossReferenceFormWidget> {
20 : Grid? _grid;
21 : dynamic _error;
22 :
23 : final LinkedScrollControllerGroup _scrollControllerGroup =
24 : LinkedScrollControllerGroup();
25 : final _controllers = <String, ScrollController>{};
26 : ScrollController? _headerController;
27 : final _keys = <String, GlobalKey<_RowMenuItemState>>{};
28 :
29 1 : @override
30 : void didChangeDependencies() {
31 1 : super.didChangeDependencies();
32 2 : if (_grid == null && _error == null) {
33 1 : _loadGrid();
34 : }
35 : }
36 :
37 1 : @override
38 : void dispose() {
39 3 : for (final controller in _controllers.values) {
40 1 : controller.dispose();
41 : }
42 2 : _headerController?.dispose();
43 1 : _headerController = null;
44 1 : super.dispose();
45 : }
46 :
47 1 : void _loadGrid() {
48 2 : setState(() {
49 1 : _error = null;
50 1 : _grid = null;
51 : });
52 2 : ApptiveGrid.getClient(context, listen: false)
53 5 : .loadGrid(gridUri: widget.component.data.gridUri)
54 2 : .then((value) {
55 2 : for (final row in value.rows) {
56 3 : _controllers[row.id]?.dispose();
57 5 : _controllers[row.id] = _scrollControllerGroup.addAndGet();
58 :
59 4 : _keys[row.id] ??= GlobalKey();
60 : }
61 1 : _headerController?.dispose();
62 3 : _headerController = _scrollControllerGroup.addAndGet();
63 2 : setState(() {
64 1 : _error = null;
65 1 : _grid = value;
66 : });
67 2 : }).catchError((error) {
68 2 : setState(() {
69 1 : _error = error;
70 1 : _grid = null;
71 : });
72 : });
73 : }
74 :
75 1 : @override
76 : Widget build(BuildContext context) {
77 1 : return GridRowDropdownButtonFormField<GridRowDropdownDataItem?>(
78 : isExpanded: true,
79 1 : items: _items(),
80 1 : onChanged: (newValue) {
81 1 : if (newValue?.entityUri != null) {
82 2 : setState(() {
83 5 : widget.component.data.value = newValue?.displayValue;
84 5 : widget.component.data.entityUri = newValue?.entityUri;
85 : });
86 : }
87 : },
88 1 : validator: (value) {
89 3 : if (widget.component.required &&
90 2 : (value?.entityUri == null || value?.displayValue == null)) {
91 4 : return '${widget.component.property} is required';
92 : } else {
93 : return null;
94 : }
95 : },
96 1 : selectedItemBuilder: _selectedItems,
97 : autovalidateMode: AutovalidateMode.onUserInteraction,
98 4 : value: widget.component.data.entityUri != null &&
99 4 : widget.component.data.value != null
100 1 : ? GridRowDropdownDataItem(
101 4 : entityUri: widget.component.data.entityUri,
102 4 : displayValue: widget.component.data.value,
103 : )
104 : : null,
105 1 : decoration: InputDecoration(
106 4 : helperText: widget.component.options.description,
107 : helperMaxLines: 100,
108 7 : labelText: widget.component.options.label ?? widget.component.property,
109 2 : errorText: _error?.toString(),
110 : ),
111 : );
112 : }
113 :
114 1 : List<GridRowDropdownMenuItem<GridRowDropdownDataItem?>>? _items() {
115 2 : if (_error != null || _grid == null) {
116 : return null;
117 : } else {
118 1 : final searchBox = GridRowDropdownMenuItem(
119 : enabled: false,
120 : value: null,
121 1 : child: Padding(
122 : padding: const EdgeInsets.symmetric(horizontal: 16.0),
123 1 : child: TextField(
124 : decoration: const InputDecoration(
125 : icon: Icon(Icons.search),
126 : hintText: 'Search',
127 : border: InputBorder.none,
128 : ),
129 1 : onChanged: (input) {
130 3 : for (final key in _keys.values) {
131 2 : key.currentState?.updateFilter(input);
132 : }
133 : },
134 : ),
135 : ),
136 : );
137 :
138 1 : final headerRow = GridRowDropdownMenuItem(
139 : enabled: false,
140 : value: null,
141 1 : child: HeaderRowWidget(
142 2 : fields: _grid!.fields,
143 1 : controller: _headerController,
144 : ),
145 : );
146 :
147 4 : final items = _grid!.rows.map((row) {
148 4 : final gridUri = widget.component.data.gridUri;
149 1 : final entityUri = EntityUri(
150 1 : user: gridUri.user,
151 1 : space: gridUri.space,
152 1 : grid: gridUri.grid,
153 1 : entity: row.id,
154 : );
155 1 : return GridRowDropdownMenuItem(
156 1 : value: GridRowDropdownDataItem(
157 : entityUri: entityUri,
158 5 : displayValue: row.entries.first.data.value.toString(),
159 : ),
160 1 : child: _RowMenuItem(
161 3 : key: _keys[row.id],
162 1 : grid: _grid!,
163 : row: row,
164 3 : controller: _controllers[row.id],
165 : ),
166 : );
167 1 : }).toList();
168 2 : return [searchBox, headerRow, ...items];
169 : }
170 : }
171 :
172 1 : List<Widget> _selectedItems(BuildContext context) {
173 1 : if (_error != null) {
174 1 : return [
175 : const Center(
176 : child: Text('ERROR'),
177 : )
178 : ];
179 1 : } else if (_grid == null) {
180 1 : return [
181 : const Center(
182 : child: Text('Loading Grid...'),
183 : )
184 : ];
185 : } else {
186 : const pleaseSelect = Text('Select an entry');
187 1 : return [
188 1 : ...[pleaseSelect, pleaseSelect],
189 2 : ..._grid!.rows
190 1 : .map(
191 2 : (row) => Text(
192 5 : row.entries.first.data.value?.toString() ?? '',
193 : maxLines: 1,
194 : overflow: TextOverflow.ellipsis,
195 : ),
196 : )
197 1 : .toList()
198 : ];
199 : }
200 : }
201 : }
202 :
203 : class _RowMenuItem extends StatefulWidget {
204 1 : const _RowMenuItem({
205 : Key? key,
206 : required this.grid,
207 : required this.row,
208 : this.controller,
209 1 : }) : super(key: key);
210 :
211 : final Grid grid;
212 : final GridRow row;
213 : final ScrollController? controller;
214 :
215 1 : @override
216 1 : _RowMenuItemState createState() => _RowMenuItemState();
217 : }
218 :
219 : class _RowMenuItemState extends State<_RowMenuItem> {
220 : String? _filter;
221 :
222 1 : void updateFilter(String filter) {
223 2 : setState(() {
224 1 : _filter = filter;
225 : });
226 : }
227 :
228 1 : @override
229 : Widget build(BuildContext context) {
230 4 : if (!widget.row.matchesFilter(_filter)) {
231 : return const SizedBox();
232 : } else {
233 3 : final index = widget.grid.rows
234 4 : .where((row) => row.matchesFilter(_filter))
235 1 : .toList()
236 3 : .indexOf(widget.row);
237 1 : return GridRowWidget(
238 2 : row: widget.row,
239 2 : controller: widget.controller,
240 2 : color: index % 2 != 0
241 3 : ? Theme.of(context).hintColor.withOpacity(0.1)
242 : : null,
243 : );
244 : }
245 : }
246 : }
247 :
248 : extension _GridRowX on GridRow {
249 1 : bool matchesFilter(String? filter) {
250 1 : if (filter == null || filter.isEmpty) return true;
251 :
252 1 : return entries
253 1 : .where(
254 1 : (entry) =>
255 2 : entry.data.schemaValue
256 1 : ?.toString()
257 1 : .toLowerCase()
258 2 : .contains(filter.toLowerCase()) ??
259 : false,
260 : )
261 1 : .isNotEmpty;
262 : }
263 : }
|