Merge branch 'main' into add-feature-history

This commit is contained in:
Ragul Raj
2024-07-20 13:59:15 +05:30
committed by GitHub
32 changed files with 897 additions and 461 deletions

View File

@ -110,22 +110,8 @@ class DashApp extends ConsumerWidget {
return Portal(
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
fontFamily: kFontFamily,
fontFamilyFallback: kFontFamilyFallback,
colorSchemeSeed: kColorSchemeSeed,
useMaterial3: true,
brightness: Brightness.light,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
darkTheme: ThemeData(
fontFamily: kFontFamily,
fontFamilyFallback: kFontFamilyFallback,
colorSchemeSeed: kColorSchemeSeed,
useMaterial3: true,
brightness: Brightness.dark,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
theme: kLightMaterialAppTheme,
darkTheme: kDarkMaterialAppTheme,
themeMode: isDarkMode ? ThemeMode.dark : ThemeMode.light,
home: Stack(
children: [

View File

@ -49,6 +49,23 @@ final kFontFamily = GoogleFonts.openSans().fontFamily;
final kFontFamilyFallback =
kIsApple ? null : <String>[GoogleFonts.notoColorEmoji().fontFamily!];
final kLightMaterialAppTheme = ThemeData(
fontFamily: kFontFamily,
fontFamilyFallback: kFontFamilyFallback,
colorSchemeSeed: kColorSchemeSeed,
useMaterial3: true,
brightness: Brightness.light,
visualDensity: VisualDensity.adaptivePlatformDensity,
);
final kDarkMaterialAppTheme = ThemeData(
fontFamily: kFontFamily,
fontFamilyFallback: kFontFamilyFallback,
colorSchemeSeed: kColorSchemeSeed,
useMaterial3: true,
brightness: Brightness.dark,
visualDensity: VisualDensity.adaptivePlatformDensity,
);
final kCodeStyle = TextStyle(
fontFamily: GoogleFonts.sourceCodePro().fontFamily,
fontFamilyFallback: kFontFamilyFallback,
@ -67,6 +84,8 @@ const kFormDataButtonLabelTextStyle = TextStyle(
);
const kTextStylePopupMenuItem = TextStyle(fontSize: 16);
final kButtonSidebarStyle = ElevatedButton.styleFrom(padding: kPh12);
const kBorderRadius4 = BorderRadius.all(Radius.circular(4));
const kBorderRadius6 = BorderRadius.all(Radius.circular(6));
const kBorderRadius8 = BorderRadius.all(Radius.circular(8));
@ -94,6 +113,7 @@ const kPt5o10 =
EdgeInsets.only(left: 10.0, right: 10.0, top: 5.0, bottom: 10.0);
const kPh4 = EdgeInsets.symmetric(horizontal: 4);
const kPh8 = EdgeInsets.symmetric(horizontal: 8);
const kPh12 = EdgeInsets.symmetric(horizontal: 12);
const kPh20 = EdgeInsets.symmetric(
horizontal: 20,
);
@ -149,6 +169,7 @@ const kPb70 = EdgeInsets.only(
const kHSpacer4 = SizedBox(width: 4);
const kHSpacer5 = SizedBox(width: 5);
const kHSpacer10 = SizedBox(width: 10);
const kHSpacer12 = SizedBox(width: 12);
const kHSpacer20 = SizedBox(width: 20);
const kHSpacer40 = SizedBox(width: 40);
const kVSpacer5 = SizedBox(height: 5);
@ -293,7 +314,21 @@ final kColorHttpMethodPut = Colors.amber.shade900;
final kColorHttpMethodPatch = kColorHttpMethodPut;
final kColorHttpMethodDelete = Colors.red.shade800;
enum ItemMenuOption { edit, delete, duplicate }
enum ItemMenuOption {
edit("Rename"),
delete("Delete"),
duplicate("Duplicate");
const ItemMenuOption(this.label);
final String label;
}
enum SidebarMenuOption {
import("Import");
const SidebarMenuOption(this.label);
final String label;
}
enum HTTPVerb { get, head, post, put, patch, delete }
@ -352,6 +387,13 @@ enum CodegenLanguage {
final String ext;
}
enum ImportFormat {
curl("cURL");
const ImportFormat(this.label);
final String label;
}
const JsonEncoder kEncoder = JsonEncoder.withIndent(' ');
const LineSplitter kSplitter = LineSplitter();
@ -671,6 +713,7 @@ const kRaiseIssue =
const kHintTextUrlCard = "Enter API endpoint like https://$kDefaultUri/";
const kLabelPlusNew = "+ New";
const kLabelMoreOptions = "More Options";
const kLabelSend = "Send";
const kLabelSending = "Sending..";
const kLabelBusy = "Busy";

View File

@ -0,0 +1,42 @@
import 'package:apidash/consts.dart';
import 'package:apidash/models/models.dart';
import 'package:apidash/utils/utils.dart';
import 'package:curl_converter/curl_converter.dart';
class CurlFileImport {
HttpRequestModel? getHttpRequestModel(String content) {
content = content.trim();
try {
final curl = Curl.parse(content);
final url = stripUriParams(curl.uri);
final method = HTTPVerb.values.byName(curl.method.toLowerCase());
final headers = curl.headers?.entries
.map((entry) => NameValueModel(
name: entry.key,
value: entry.value,
))
.toList();
final params = curl.uri.queryParameters.entries
.map((entry) => NameValueModel(
name: entry.key,
value: entry.value,
))
.toList();
// TODO: parse curl data to determine the type of body
final body = curl.data;
return HttpRequestModel(
method: method,
url: url,
headers: headers,
params: params,
body: body,
);
} catch (e) {
return null;
}
}
}

View File

@ -0,0 +1,17 @@
import 'package:apidash/consts.dart';
import 'package:apidash/models/models.dart';
import 'curl/curl.dart';
class Importer {
Future<HttpRequestModel?> getHttpRequestModel(
ImportFormat fileType,
String content,
) async {
switch (fileType) {
case ImportFormat.curl:
return CurlFileImport().getHttpRequestModel(content);
default:
return null;
}
}
}

View File

@ -69,6 +69,22 @@ class CollectionStateNotifier
ref.read(hasUnsavedChangesProvider.notifier).state = true;
}
void addRequestModel(HttpRequestModel httpRequestModel) {
final id = getNewUuid();
final newRequestModel = RequestModel(
id: id,
httpRequestModel: httpRequestModel,
);
var map = {...state!};
map[id] = newRequestModel;
state = map;
ref
.read(requestSequenceProvider.notifier)
.update((state) => [id, ...state]);
ref.read(selectedIdStateProvider.notifier).state = newRequestModel.id;
ref.read(hasUnsavedChangesProvider.notifier).state = true;
}
void reorder(int oldIdx, int newIdx) {
var itemIds = ref.read(requestSequenceProvider);
final itemId = itemIds.removeAt(oldIdx);

View File

@ -1,3 +1,4 @@
import 'package:apidash/consts.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -32,3 +33,5 @@ final nameTextFieldFocusNodeProvider =
final collectionSearchQueryProvider = StateProvider<String>((ref) => '');
final environmentSearchQueryProvider = StateProvider<String>((ref) => '');
final importFormatStateProvider =
StateProvider<ImportFormat>((ref) => ImportFormat.curl);

View File

@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:extended_text_field/extended_text_field.dart';
import 'package:apidash/consts.dart';
import 'envvar_span.dart';
class EnvRegExpSpanBuilder extends RegExpSpecialTextSpanBuilder {
@override
List<RegExpSpecialText> get regExps => <RegExpSpecialText>[
RegExpEnvText(),
];
}
class RegExpEnvText extends RegExpSpecialText {
@override
RegExp get regExp => kEnvVarRegEx;
@override
InlineSpan finishText(int start, Match match,
{TextStyle? textStyle, SpecialTextGestureTapCallback? onTap}) {
final String value = '${match[0]}';
return ExtendedWidgetSpan(
actualText: value,
start: start,
alignment: PlaceholderAlignment.middle,
child: EnvVarSpan(variableKey: value.substring(2, value.length - 2)),
);
}
}

View File

@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart';
import 'package:extended_text_field/extended_text_field.dart';
import 'env_regexp_span_builder.dart';
import 'env_trigger_options.dart';
class EnvironmentTriggerField extends StatefulWidget {
const EnvironmentTriggerField({
super.key,
required this.keyId,
this.initialValue,
this.onChanged,
this.onFieldSubmitted,
this.style,
this.decoration,
this.optionsWidthFactor,
});
final String keyId;
final String? initialValue;
final void Function(String)? onChanged;
final void Function(String)? onFieldSubmitted;
final TextStyle? style;
final InputDecoration? decoration;
final double? optionsWidthFactor;
@override
State<EnvironmentTriggerField> createState() =>
_EnvironmentTriggerFieldState();
}
class _EnvironmentTriggerFieldState extends State<EnvironmentTriggerField> {
final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode();
@override
void initState() {
super.initState();
controller.text = widget.initialValue ?? '';
controller.selection =
TextSelection.collapsed(offset: controller.text.length);
}
@override
void dispose() {
controller.dispose();
focusNode.dispose();
super.dispose();
}
@override
void didUpdateWidget(EnvironmentTriggerField oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialValue != widget.initialValue) {
controller.text = widget.initialValue ?? "";
controller.selection =
TextSelection.collapsed(offset: controller.text.length);
}
}
@override
Widget build(BuildContext context) {
return MultiTriggerAutocomplete(
key: Key(widget.keyId),
textEditingController: controller,
focusNode: focusNode,
optionsWidthFactor: widget.optionsWidthFactor,
autocompleteTriggers: [
AutocompleteTrigger(
trigger: '{',
triggerEnd: "}}",
triggerOnlyAfterSpace: false,
optionsViewBuilder: (context, autocompleteQuery, controller) {
return EnvironmentAutocompleteOptions(
query: autocompleteQuery.query,
onSuggestionTap: (suggestion) {
final autocomplete = MultiTriggerAutocomplete.of(context);
autocomplete.acceptAutocompleteOption(
'{${suggestion.variable.key}',
);
widget.onChanged?.call(controller.text);
});
}),
AutocompleteTrigger(
trigger: '{{',
triggerEnd: "}}",
triggerOnlyAfterSpace: false,
optionsViewBuilder: (context, autocompleteQuery, controller) {
return EnvironmentAutocompleteOptions(
query: autocompleteQuery.query,
onSuggestionTap: (suggestion) {
final autocomplete = MultiTriggerAutocomplete.of(context);
autocomplete.acceptAutocompleteOption(
suggestion.variable.key,
);
widget.onChanged?.call(controller.text);
});
}),
],
fieldViewBuilder: (context, textEditingController, focusnode) {
return ExtendedTextField(
controller: textEditingController,
focusNode: focusnode,
decoration: widget.decoration,
style: widget.style,
onChanged: widget.onChanged,
onSubmitted: widget.onFieldSubmitted,
specialTextSpanBuilder: EnvRegExpSpanBuilder(),
);
},
);
}
}

View File

@ -0,0 +1,66 @@
import 'package:apidash/consts.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:apidash/models/models.dart';
import 'package:apidash/providers/providers.dart';
import 'package:apidash/utils/utils.dart';
import 'envvar_indicator.dart';
class EnvironmentAutocompleteOptions extends ConsumerWidget {
const EnvironmentAutocompleteOptions({
super.key,
required this.query,
required this.onSuggestionTap,
});
final String query;
final ValueSetter<EnvironmentVariableSuggestion> onSuggestionTap;
@override
Widget build(BuildContext context, WidgetRef ref) {
final envMap = ref.watch(availableEnvironmentVariablesStateProvider);
final activeEnvironmentId = ref.watch(activeEnvironmentIdStateProvider);
final suggestions =
getEnvironmentTriggerSuggestions(query, envMap, activeEnvironmentId);
return suggestions == null || suggestions.isEmpty
? const SizedBox.shrink()
: ClipRRect(
borderRadius: kBorderRadius8,
child: Material(
type: MaterialType.card,
elevation: 8,
child: ConstrainedBox(
constraints:
const BoxConstraints(maxHeight: kSuggestionsMenuMaxHeight),
child: Ink(
width: kSuggestionsMenuWidth,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: kBorderRadius8,
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
child: ListView.separated(
shrinkWrap: true,
itemCount: suggestions.length,
separatorBuilder: (context, index) =>
const Divider(height: 2),
itemBuilder: (context, index) {
final suggestion = suggestions[index];
return ListTile(
dense: true,
leading: EnvVarIndicator(suggestion: suggestion),
title: Text(suggestion.variable.key),
subtitle: Text(suggestion.variable.value),
onTap: () => onSuggestionTap(suggestion),
);
},
),
),
),
),
);
}
}

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:apidash/consts.dart';
import 'environment_field.dart';
import 'env_trigger_field.dart';
class EnvCellField extends StatelessWidget {
const EnvCellField({
@ -21,7 +21,7 @@ class EnvCellField extends StatelessWidget {
@override
Widget build(BuildContext context) {
var clrScheme = colorScheme ?? Theme.of(context).colorScheme;
return EnvironmentField(
return EnvironmentTriggerField(
keyId: keyId,
initialValue: initialValue,
style: kCodeStyle.copyWith(

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:apidash/consts.dart';
import 'environment_field.dart';
import 'env_trigger_field.dart';
class EnvURLField extends StatelessWidget {
const EnvURLField({
@ -18,7 +18,7 @@ class EnvURLField extends StatelessWidget {
@override
Widget build(BuildContext context) {
return EnvironmentField(
return EnvironmentTriggerField(
keyId: "url-$selectedId",
initialValue: initialValue,
style: kCodeStyle,
@ -33,6 +33,7 @@ class EnvURLField extends StatelessWidget {
),
onChanged: onChanged,
onFieldSubmitted: onFieldSubmitted,
optionsWidthFactor: 1,
);
}
}

View File

@ -1,49 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:apidash/providers/providers.dart';
import 'package:apidash/utils/envvar_utils.dart';
import 'package:apidash/widgets/widgets.dart';
class EnvironmentField extends HookConsumerWidget {
const EnvironmentField({
super.key,
required this.keyId,
this.initialValue,
this.onChanged,
this.onFieldSubmitted,
this.style,
this.decoration,
});
final String keyId;
final String? initialValue;
final void Function(String)? onChanged;
final void Function(String)? onFieldSubmitted;
final TextStyle? style;
final InputDecoration? decoration;
@override
Widget build(BuildContext context, WidgetRef ref) {
final mentionValue = ref.watch(environmentFieldEditStateProvider);
final envMap = ref.watch(availableEnvironmentVariablesStateProvider);
final activeEnvironmentId = ref.watch(activeEnvironmentIdStateProvider);
final initialMentions =
getMentions(initialValue, envMap, activeEnvironmentId);
final suggestions = getEnvironmentVariableSuggestions(
mentionValue, envMap, activeEnvironmentId);
return EnvironmentFieldBase(
key: Key(keyId),
mentionValue: mentionValue,
onMentionValueChanged: (value) {
ref.read(environmentFieldEditStateProvider.notifier).state = value;
},
initialValue: initialValue,
initialMentions: initialMentions,
suggestions: suggestions,
onChanged: onChanged,
onFieldSubmitted: onFieldSubmitted,
style: style,
decoration: decoration,
);
}
}

View File

@ -3,7 +3,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:apidash/consts.dart';
import 'package:apidash/models/models.dart';
import 'package:apidash/providers/providers.dart';
import 'package:apidash/utils/utils.dart';
import 'envvar_popover.dart';
@ -11,10 +10,10 @@ import 'envvar_popover.dart';
class EnvVarSpan extends HookConsumerWidget {
const EnvVarSpan({
super.key,
required this.suggestion,
required this.variableKey,
});
final EnvironmentVariableSuggestion suggestion;
final String variableKey;
@override
Widget build(BuildContext context, WidgetRef ref) {
@ -22,20 +21,19 @@ class EnvVarSpan extends HookConsumerWidget {
final envMap = ref.watch(availableEnvironmentVariablesStateProvider);
final activeEnvironmentId = ref.watch(activeEnvironmentIdStateProvider);
final currentSuggestion =
getCurrentVariableStatus(suggestion, envMap, activeEnvironmentId);
final suggestion =
getVariableStatus(variableKey, envMap, activeEnvironmentId);
final showPopover = useState(false);
final isMissingVariable = currentSuggestion.isUnknown;
final isMissingVariable = suggestion.isUnknown;
final String scope = isMissingVariable
? 'unknown'
: getEnvironmentTitle(
environments?[currentSuggestion.environmentId]?.name);
: getEnvironmentTitle(environments?[suggestion.environmentId]?.name);
final colorScheme = Theme.of(context).colorScheme;
var text = Text(
'{{${currentSuggestion.variable.key}}}',
'{{${suggestion.variable.key}}}',
style: TextStyle(
color: isMissingVariable ? colorScheme.error : colorScheme.primary,
fontWeight: FontWeight.w600),
@ -50,7 +48,7 @@ class EnvVarSpan extends HookConsumerWidget {
onExit: (_) {
showPopover.value = false;
},
child: EnvVarPopover(suggestion: currentSuggestion, scope: scope),
child: EnvVarPopover(suggestion: suggestion, scope: scope),
),
anchor: const Aligned(
follower: Alignment.bottomCenter,

View File

@ -2,16 +2,23 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:apidash/providers/providers.dart';
import 'package:apidash/extensions/extensions.dart';
import 'package:apidash/widgets/widgets.dart';
import 'package:apidash/consts.dart';
import 'sidebar_save_button.dart';
class SidebarHeader extends ConsumerWidget {
const SidebarHeader({super.key, this.onAddNew});
final Function()? onAddNew;
const SidebarHeader({
super.key,
this.onAddNew,
this.onImport,
});
final VoidCallback? onAddNew;
final VoidCallback? onImport;
@override
Widget build(BuildContext context, WidgetRef ref) {
final mobileScaffoldKey = ref.read(mobileScaffoldKeyStateProvider);
return Padding(
padding: kPe8,
child: Row(
@ -20,16 +27,28 @@ class SidebarHeader extends ConsumerWidget {
const Spacer(),
ElevatedButton(
onPressed: onAddNew,
style: kButtonSidebarStyle,
child: const Text(
kLabelPlusNew,
style: kTextStyleButton,
),
),
kHSpacer4,
SizedBox(
width: 24,
child: SidebarTopMenu(
tooltip: kLabelMoreOptions,
onSelected: (option) => switch (option) {
SidebarMenuOption.import => onImport?.call(),
},
),
),
context.width <= kMinWindowSize.width
? IconButton(
style: IconButton.styleFrom(
padding: const EdgeInsets.all(4),
minimumSize: const Size(30, 30)),
padding: const EdgeInsets.all(4),
minimumSize: const Size(30, 30),
),
onPressed: () {
mobileScaffoldKey.currentState?.closeDrawer();
},

View File

@ -1,3 +1,4 @@
import 'package:apidash/importer/importer.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:apidash/providers/providers.dart';
@ -7,6 +8,8 @@ import 'package:apidash/models/models.dart';
import 'package:apidash/consts.dart';
import '../common_widgets/common_widgets.dart';
final kImporter = Importer();
class CollectionPane extends ConsumerWidget {
const CollectionPane({
super.key,
@ -32,10 +35,38 @@ class CollectionPane extends ConsumerWidget {
onAddNew: () {
ref.read(collectionStateNotifierProvider.notifier).add();
},
onImport: () {
showImportDialog(
context: context,
importFormat: ref.watch(importFormatStateProvider),
onImportFormatChange: (format) {
if (format != null) {
ref.read(importFormatStateProvider.notifier).state = format;
}
},
onFileDropped: (file) {
final importFormatType = ref.read(importFormatStateProvider);
file.readAsString().then((content) {
kImporter
.getHttpRequestModel(importFormatType, content)
.then((importedRequestModel) {
if (importedRequestModel != null) {
ref
.read(collectionStateNotifierProvider.notifier)
.addRequestModel(importedRequestModel);
} else {
// TODO: Throw an error, unable to parse
}
});
});
Navigator.of(context).pop();
},
);
},
),
kVSpacer10,
SidebarFilter(
filterHintText: "Filter by name or url",
filterHintText: "Filter by name or url",
onFilterFieldChanged: (value) {
ref.read(collectionSearchQueryProvider.notifier).state =
value.toLowerCase();

View File

@ -1,8 +1,6 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:apidash/consts.dart';
import 'package:apidash/models/models.dart';
import '../screens/common_widgets/common_widgets.dart';
String getEnvironmentTitle(String? name) {
if (name == null || name.trim() == "") {
@ -92,92 +90,16 @@ HttpRequestModel substituteHttpRequestModel(
return newRequestModel;
}
List<(String, Object?, Widget?)> getMentions(
String? text,
Map<String, List<EnvironmentVariableModel>> envMap,
String? activeEnvironmentId) {
if (text == null) {
return [];
}
final mentions = <(String, Object?, Widget?)>[];
final matches = kEnvVarRegEx.allMatches(text);
for (final match in matches) {
final variableName = match.group(1);
EnvironmentVariableModel? variable;
String? environmentId;
for (final entry in envMap.entries) {
variable = entry.value.firstWhereOrNull((v) => v.key == variableName);
if (variable != null) {
environmentId = entry.key;
break;
}
}
final suggestion = EnvironmentVariableSuggestion(
environmentId: variable == null ? "unknown" : environmentId!,
variable: variable ??
kEnvironmentVariableEmptyModel.copyWith(
key: variableName ?? "",
),
isUnknown: variable == null);
mentions.add((
'{{${variable?.key ?? variableName}}}',
suggestion,
EnvVarSpan(suggestion: suggestion)
));
}
return mentions;
}
EnvironmentVariableSuggestion getCurrentVariableStatus(
EnvironmentVariableSuggestion currentSuggestion,
Map<String, List<EnvironmentVariableModel>> envMap,
String? activeEnvironmentId) {
if (activeEnvironmentId != null) {
final variable = envMap[activeEnvironmentId]!
.firstWhereOrNull((v) => v.key == currentSuggestion.variable.key);
if (variable != null) {
return currentSuggestion.copyWith(
environmentId: activeEnvironmentId,
variable: variable,
isUnknown: false,
);
}
}
final globalVariable = envMap[kGlobalEnvironmentId]
?.firstWhereOrNull((v) => v.key == currentSuggestion.variable.key);
if (globalVariable != null) {
return currentSuggestion.copyWith(
environmentId: kGlobalEnvironmentId,
variable: globalVariable,
isUnknown: false,
);
}
return currentSuggestion.copyWith(
isUnknown: true,
variable: currentSuggestion.variable.copyWith(value: "unknown"));
}
List<EnvironmentVariableSuggestion>? getEnvironmentVariableSuggestions(
List<EnvironmentVariableSuggestion>? getEnvironmentTriggerSuggestions(
String? query,
Map<String, List<EnvironmentVariableModel>> envMap,
String? activeEnvironmentId) {
if (query == null || kEnvVarRegEx.hasMatch(query)) return null;
query = query.substring(1);
final suggestions = <EnvironmentVariableSuggestion>[];
final Set<String> addedVariableKeys = {};
if (activeEnvironmentId != null && envMap[activeEnvironmentId] != null) {
for (final variable in envMap[activeEnvironmentId]!) {
if ((query.isEmpty || variable.key.contains(query)) &&
if ((query!.isEmpty || variable.key.contains(query)) &&
!addedVariableKeys.contains(variable.key)) {
suggestions.add(EnvironmentVariableSuggestion(
environmentId: activeEnvironmentId, variable: variable));
@ -197,3 +119,36 @@ List<EnvironmentVariableSuggestion>? getEnvironmentVariableSuggestions(
return suggestions;
}
EnvironmentVariableSuggestion getVariableStatus(
String key,
Map<String, List<EnvironmentVariableModel>> envMap,
String? activeEnvironmentId) {
if (activeEnvironmentId != null) {
final variable =
envMap[activeEnvironmentId]!.firstWhereOrNull((v) => v.key == key);
if (variable != null) {
return EnvironmentVariableSuggestion(
environmentId: activeEnvironmentId,
variable: variable,
isUnknown: false,
);
}
}
final globalVariable =
envMap[kGlobalEnvironmentId]?.firstWhereOrNull((v) => v.key == key);
if (globalVariable != null) {
return EnvironmentVariableSuggestion(
environmentId: kGlobalEnvironmentId,
variable: globalVariable,
isUnknown: false,
);
}
return EnvironmentVariableSuggestion(
isUnknown: true,
environmentId: "unknown",
variable: EnvironmentVariableModel(
key: key, type: EnvironmentVariableType.variable, value: "unknown"));
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:apidash/consts.dart';
import 'package:apidash/utils/utils.dart';
import 'menus.dart' show ItemCardMenu;
import 'menu_item_card.dart';
class SidebarEnvironmentCard extends StatelessWidget {
const SidebarEnvironmentCard({

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:apidash/consts.dart';
import 'package:apidash/utils/utils.dart';
import 'menus.dart' show ItemCardMenu;
import 'menu_item_card.dart';
import 'texts.dart' show MethodBox;
class SidebarRequestCard extends StatelessWidget {

View File

@ -0,0 +1,39 @@
import 'package:apidash/consts.dart';
import 'package:flutter/material.dart';
import 'package:file_selector/file_selector.dart';
import 'drag_and_drop_area.dart';
import 'dropdown_import_format.dart';
showImportDialog({
required BuildContext context,
required ImportFormat importFormat,
Function(ImportFormat?)? onImportFormatChange,
Function(XFile)? onFileDropped,
}) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
contentPadding: const EdgeInsets.all(12),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("Import "),
DropdownButtonImportFormat(
importFormat: importFormat,
onChanged: onImportFormatChange,
),
],
),
DragAndDropArea(
onFileDropped: onFileDropped,
),
],
),
);
},
);
}

View File

@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:file_selector/file_selector.dart';
import 'package:apidash/utils/utils.dart';
import 'package:apidash/consts.dart';
class DragAndDropArea extends StatefulWidget {
final Function(XFile)? onFileDropped;
const DragAndDropArea({
super.key,
this.onFileDropped,
});
@override
State<DragAndDropArea> createState() => _DragAndDropAreaState();
}
class _DragAndDropAreaState extends State<DragAndDropArea> {
final List<XFile> _list = [];
bool _dragging = false;
@override
Widget build(BuildContext context) {
return DropTarget(
onDragDone: (detail) {
setState(() {
_list.addAll(detail.files);
});
widget.onFileDropped?.call(detail.files[0]);
},
onDragEntered: (detail) {
setState(() {
_dragging = true;
});
},
onDragExited: (detail) {
setState(() {
_dragging = false;
});
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22),
color: _dragging
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surface,
border: Border.all(
color: Theme.of(context).colorScheme.primaryContainer,
),
),
width: 600,
height: 400,
child: _list.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 150,
child: ElevatedButton.icon(
icon: const Icon(
Icons.snippet_folder_rounded,
size: 20,
),
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(kDataTableRowHeight),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
),
onPressed: () async {
var pickedResult = await pickFile();
if (pickedResult != null &&
pickedResult.path.isNotEmpty) {
widget.onFileDropped?.call(pickedResult);
}
},
label: const Text(
kLabelSelectFile,
overflow: TextOverflow.ellipsis,
style: kFormDataButtonLabelTextStyle,
),
),
),
kVSpacer10,
const Text("Select or drop the file here"),
],
))
: Text(_list.map((e) => e.path).join("\n")),
),
);
}
}

View File

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:apidash/consts.dart';
class DropdownButtonImportFormat extends StatelessWidget {
const DropdownButtonImportFormat({
super.key,
required this.importFormat,
this.onChanged,
});
final ImportFormat importFormat;
final void Function(ImportFormat?)? onChanged;
@override
Widget build(BuildContext context) {
final surfaceColor = Theme.of(context).colorScheme.surface;
return DropdownButton<ImportFormat>(
isExpanded: false,
focusColor: surfaceColor,
value: importFormat,
icon: const Icon(
Icons.unfold_more_rounded,
size: 16,
),
elevation: 4,
style: kCodeStyle.copyWith(
color: Theme.of(context).colorScheme.primary,
),
underline: Container(
height: 0,
),
onChanged: onChanged,
borderRadius: kBorderRadius12,
items: ImportFormat.values
.map<DropdownMenuItem<ImportFormat>>((ImportFormat value) {
return DropdownMenuItem<ImportFormat>(
value: value,
child: Padding(
padding: kPs8,
child: Text(
value.label,
style: kTextStyleButton,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
);
}).toList(),
);
}
}

View File

@ -1,104 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:mention_tag_text_field/mention_tag_text_field.dart';
import 'package:apidash/models/models.dart';
import 'package:apidash/widgets/field_mention.dart';
import '../screens/common_widgets/common_widgets.dart';
import 'suggestions_menu.dart';
class EnvironmentFieldBase extends StatefulHookWidget {
const EnvironmentFieldBase({
super.key,
this.initialValue,
this.onChanged,
this.onFieldSubmitted,
this.style,
this.decoration,
this.initialMentions,
this.suggestions,
this.mentionValue,
required this.onMentionValueChanged,
});
final String? initialValue;
final void Function(String)? onChanged;
final void Function(String)? onFieldSubmitted;
final TextStyle? style;
final InputDecoration? decoration;
final List<(String, Object?, Widget?)>? initialMentions;
final List<EnvironmentVariableSuggestion>? suggestions;
final String? mentionValue;
final void Function(String?) onMentionValueChanged;
@override
State<EnvironmentFieldBase> createState() =>
_EnvironmentAutocompleteFieldBaseState();
}
class _EnvironmentAutocompleteFieldBaseState
extends State<EnvironmentFieldBase> {
final MentionTagTextEditingController controller =
MentionTagTextEditingController();
final FocusNode focusNode = FocusNode();
@override
void initState() {
super.initState();
controller.text = widget.initialValue ?? "";
}
@override
Widget build(BuildContext context) {
final isSuggestionsVisible = useState(false);
return PortalTarget(
visible: isSuggestionsVisible.value && focusNode.hasFocus,
portalFollower: SuggestionsMenu(
mentionController: controller,
suggestions: widget.suggestions,
suggestionBuilder: (context, index) {
final suggestion = widget.suggestions![index];
return ListTile(
dense: true,
leading: EnvVarIndicator(suggestion: suggestion),
title: Text(suggestion.variable.key),
subtitle: Text(suggestion.variable.value),
onTap: () {
controller.addMention(
label: '{${suggestion.variable.key}}}',
data: suggestion,
stylingWidget: EnvVarSpan(suggestion: suggestion));
widget.onChanged?.call(controller.text);
widget.onMentionValueChanged.call(null);
isSuggestionsVisible.value = false;
},
);
},
),
anchor: const Aligned(
follower: Alignment.topLeft,
target: Alignment.bottomLeft,
backup: Aligned(
follower: Alignment.bottomLeft,
target: Alignment.topLeft,
),
),
child: MentionField(
focusNode: focusNode,
controller: controller,
initialMentions: widget.initialMentions ?? [],
mentionValue: widget.mentionValue,
onMentionValueChanged: widget.onMentionValueChanged,
isSuggestionsVisible: isSuggestionsVisible,
onChanged: widget.onChanged,
onFieldSubmitted: widget.onFieldSubmitted,
style: widget.style,
decoration: widget.decoration,
mentionStart: const ['{'],
maxWords: 1,
),
);
}
}

View File

@ -1,85 +0,0 @@
import 'package:flutter/material.dart';
import 'package:mention_tag_text_field/mention_tag_text_field.dart';
class MentionField extends StatelessWidget {
const MentionField({
super.key,
required this.focusNode,
required this.controller,
required this.initialMentions,
required this.mentionValue,
required this.onMentionValueChanged,
required this.isSuggestionsVisible,
this.onChanged,
this.onFieldSubmitted,
this.style,
this.decoration,
required this.mentionStart,
this.mentionBreak = "",
this.maxWords,
this.allowDecrement = false,
this.allowEmbedding = true,
this.showMentionStartSymbol = false,
});
final FocusNode focusNode;
final MentionTagTextEditingController controller;
final List<(String, Object?, Widget?)> initialMentions;
final String? mentionValue;
final void Function(String?) onMentionValueChanged;
final ValueNotifier<bool> isSuggestionsVisible;
final void Function(String)? onChanged;
final void Function(String)? onFieldSubmitted;
final TextStyle? style;
final InputDecoration? decoration;
final List<String> mentionStart;
final String mentionBreak;
final int? maxWords;
final bool allowDecrement;
final bool allowEmbedding;
final bool showMentionStartSymbol;
void onMention(String? value) {
onMentionValueChanged.call(value);
if (value != null) {
isSuggestionsVisible.value = true;
} else {
isSuggestionsVisible.value = false;
}
}
@override
Widget build(BuildContext context) {
return MentionTagTextField(
focusNode: focusNode,
onTap: () {
focusNode.requestFocus();
},
onTapOutside: (event) {
focusNode.unfocus();
isSuggestionsVisible.value = false;
},
controller: controller,
style: style,
initialMentions: initialMentions,
onMention: onMention,
onChanged: (value) {
onChanged?.call(controller.text);
},
onEditingComplete: () {
focusNode.unfocus();
onFieldSubmitted?.call(controller.text);
isSuggestionsVisible.value = false;
},
decoration: decoration,
mentionTagDecoration: MentionTagDecoration(
mentionStart: mentionStart,
mentionBreak: mentionBreak,
maxWords: maxWords,
allowDecrement: allowDecrement,
allowEmbedding: allowEmbedding,
showMentionStartSymbol: showMentionStartSymbol,
),
);
}
}

View File

@ -29,20 +29,14 @@ class ItemCardMenu extends StatelessWidget {
offset: offset,
onSelected: onSelected,
shape: shape,
itemBuilder: (BuildContext context) => <PopupMenuEntry<ItemMenuOption>>[
const PopupMenuItem<ItemMenuOption>(
value: ItemMenuOption.edit,
child: Text('Rename'),
),
const PopupMenuItem<ItemMenuOption>(
value: ItemMenuOption.delete,
child: Text('Delete'),
),
const PopupMenuItem<ItemMenuOption>(
value: ItemMenuOption.duplicate,
child: Text('Duplicate'),
),
],
itemBuilder: (BuildContext context) => ItemMenuOption.values
.map<PopupMenuEntry<ItemMenuOption>>(
(e) => PopupMenuItem<ItemMenuOption>(
value: e,
child: Text(e.label),
),
)
.toList(),
child: child,
);
}

View File

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:apidash/consts.dart';
class SidebarTopMenu extends StatelessWidget {
const SidebarTopMenu({
super.key,
this.onSelected,
this.child,
this.offset = Offset.zero,
this.splashRadius = 14,
this.tooltip,
this.shape,
});
final Widget? child;
final Offset offset;
final double splashRadius;
final String? tooltip;
final ShapeBorder? shape;
final Function(SidebarMenuOption)? onSelected;
@override
Widget build(BuildContext context) {
return PopupMenuButton<SidebarMenuOption>(
tooltip: tooltip,
padding: EdgeInsets.zero,
splashRadius: splashRadius,
icon: const Icon(Icons.more_vert),
iconSize: 14,
offset: offset,
onSelected: onSelected,
shape: shape,
itemBuilder: (BuildContext context) => SidebarMenuOption.values
.map<PopupMenuEntry<SidebarMenuOption>>(
(e) => PopupMenuItem<SidebarMenuOption>(
value: e,
child: Text(e.label),
),
)
.toList(),
child: child,
);
}
}

View File

@ -1,53 +0,0 @@
import 'package:flutter/material.dart';
import 'package:mention_tag_text_field/mention_tag_text_field.dart';
import 'package:apidash/consts.dart';
class SuggestionsMenu extends StatelessWidget {
const SuggestionsMenu({
super.key,
required this.mentionController,
required this.suggestions,
required this.suggestionBuilder,
this.menuWidth = kSuggestionsMenuWidth,
this.menuMaxHeight = kSuggestionsMenuMaxHeight,
});
final MentionTagTextEditingController mentionController;
final List? suggestions;
final double menuWidth;
final double menuMaxHeight;
final Widget? Function(BuildContext, int) suggestionBuilder;
@override
Widget build(BuildContext context) {
return suggestions == null || suggestions!.isEmpty
? const SizedBox.shrink()
: ClipRRect(
borderRadius: kBorderRadius8,
child: Material(
type: MaterialType.card,
elevation: 8,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: menuMaxHeight),
child: Ink(
width: menuWidth,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: kBorderRadius8,
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
child: ListView.separated(
shrinkWrap: true,
itemCount: suggestions?.length ?? 0,
separatorBuilder: (context, index) =>
const Divider(height: 2),
itemBuilder: suggestionBuilder,
),
),
),
),
);
}
}

View File

@ -12,26 +12,28 @@ export 'checkbox.dart';
export 'code_previewer.dart';
export 'codegen_previewer.dart';
export 'dialog_about.dart';
export 'dialog_import.dart';
export 'dialog_rename.dart';
export 'drag_and_drop_area.dart';
export 'dropdown_codegen.dart';
export 'dropdown_content_type.dart';
export 'dropdown_formdata.dart';
export 'dropdown_http_method.dart';
export 'dropdown_import_format.dart';
export 'editor_json.dart';
export 'editor.dart';
export 'environment_field_base.dart';
export 'error_message.dart';
export 'field_cell_obscurable.dart';
export 'field_cell.dart';
export 'field_header.dart';
export 'field_json_search.dart';
export 'field_mention.dart';
export 'field_raw.dart';
export 'field_url.dart';
export 'intro_message.dart';
export 'json_previewer.dart';
export 'markdown.dart';
export 'menus.dart';
export 'menu_item_card.dart';
export 'menu_sidebar_top.dart';
export 'overlay_widget.dart';
export 'popup_menu_codegen.dart';
export 'popup_menu_env.dart';

View File

@ -153,6 +153,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.3"
checks:
dependency: transitive
description:
name: checks
sha256: aad431b45a8ae2fa26db8c22e385b9cdec73f72986a1d9d9f2017f4c39ecf5c9
url: "https://pub.dev"
source: hosted
version: "0.3.0"
cli_util:
dependency: transitive
description:
@ -233,6 +241,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
curl_converter:
dependency: "direct main"
description:
path: "."
ref: "726e8cd04aeb326211af27f75920be5b21c90bb4"
resolved-ref: "726e8cd04aeb326211af27f75920be5b21c90bb4"
url: "https://github.com/foss42/curl_converter.git"
source: git
version: "1.0.4"
dart_style:
dependency: "direct main"
description:
@ -241,6 +258,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.6"
dartx:
dependency: transitive
description:
name: dartx
sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
data_table_2:
dependency: "direct main"
description:
@ -249,6 +274,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.5.15"
desktop_drop:
dependency: "direct main"
description:
name: desktop_drop
sha256: d55a010fe46c8e8fcff4ea4b451a9ff84a162217bdb3b2a0aa1479776205e15d
url: "https://pub.dev"
source: hosted
version: "0.4.4"
equatable:
dependency: transitive
description:
name: equatable
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
url: "https://pub.dev"
source: hosted
version: "2.0.5"
eventify:
dependency: transitive
description:
@ -258,7 +299,7 @@ packages:
source: hosted
version: "1.0.1"
extended_text_field:
dependency: transitive
dependency: "direct main"
description:
name: extended_text_field
sha256: "954c7eea1e82728a742f7ddf09b9a51cef087d4f52b716ba88cb3eb78ccd7c6e"
@ -390,6 +431,11 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_driver:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_hooks:
dependency: "direct main"
description:
@ -536,6 +582,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.2.0"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
fvp:
dependency: "direct main"
description:
@ -648,6 +699,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.2.0"
integration_test:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
intl:
dependency: "direct main"
description:
@ -825,14 +881,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.8.0"
mention_tag_text_field:
dependency: "direct main"
description:
name: mention_tag_text_field
sha256: "9768a0a6fe5771cb8eb98f94b26b4c595ca2487b0eb28b9d5624f8d71a2ac17a"
url: "https://pub.dev"
source: hosted
version: "0.0.5"
meta:
dependency: transitive
description:
@ -873,6 +921,23 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.2.2"
multi_trigger_autocomplete:
dependency: "direct main"
description:
path: "."
ref: cb22bab30dd14452d184bc6ad3bb41b612b22c70
resolved-ref: cb22bab30dd14452d184bc6ad3bb41b612b22c70
url: "https://github.com/foss42/multi_trigger_autocomplete.git"
source: git
version: "1.0.1"
nanoid2:
dependency: transitive
description:
name: nanoid2
sha256: "35b5048f836652a1d711db0d716bdee59fcaaa4c37792db8b3568da4f7feb2f9"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
nested:
dependency: transitive
description:
@ -1065,6 +1130,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.12.0"
process:
dependency: transitive
description:
name: process
sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
url: "https://pub.dev"
source: hosted
version: "5.0.2"
provider:
dependency: "direct main"
description:
@ -1161,6 +1234,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.4"
shlex:
dependency: transitive
description:
name: shlex
sha256: "733dde67711b5a196ae753caa166f51ea8d0f3a8080ab5b8520172af2465f478"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
sky_engine:
dependency: transitive
description: flutter
@ -1206,6 +1287,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.0"
spot:
dependency: "direct dev"
description:
name: spot
sha256: "648cd3e9f9b336d005a4dcde24538e44edc72b8d548e0416fa93c0541655f219"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
sprintf:
dependency: transitive
description:
@ -1254,6 +1343,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
sync_http:
dependency: transitive
description:
name: sync_http
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
term_glyph:
dependency: transitive
description:
@ -1294,6 +1391,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
time:
dependency: transitive
description:
name: time
sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221
url: "https://pub.dev"
source: hosted
version: "2.1.4"
timing:
dependency: transitive
description:
@ -1486,6 +1591,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.5"
webdriver:
dependency: transitive
description:
name: webdriver
sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
@ -1544,5 +1657,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.4.0 <4.0.0"
dart: ">=3.4.0 <3.999.0"
flutter: ">=3.22.0"

View File

@ -63,8 +63,17 @@ dependencies:
hooks_riverpod: ^2.5.1
flutter_hooks: ^0.20.5
flutter_portal: ^1.1.4
mention_tag_text_field: ^0.0.5
intl: ^0.19.0
multi_trigger_autocomplete:
git:
url: https://github.com/foss42/multi_trigger_autocomplete.git
ref: cb22bab30dd14452d184bc6ad3bb41b612b22c70
extended_text_field: ^15.0.0
desktop_drop: ^0.4.4
curl_converter:
git:
url: https://github.com/foss42/curl_converter.git
ref: 726e8cd04aeb326211af27f75920be5b21c90bb4
dependency_overrides:
web: ^0.5.0
@ -79,6 +88,7 @@ dev_dependencies:
build_runner: ^2.4.11
freezed: ^2.5.2
json_serializable: ^6.7.1
spot: ^0.13.0
flutter:
uses-material-design: true

View File

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:spot/spot.dart';
import 'package:apidash/consts.dart';
import 'package:apidash/providers/providers.dart';
import 'package:apidash/screens/common_widgets/common_widgets.dart';
import 'package:apidash/screens/dashboard.dart';
@ -14,13 +15,12 @@ import 'package:apidash/screens/home_page/home_page.dart';
import 'package:apidash/screens/settings_page.dart';
import 'package:apidash/services/hive_services.dart';
import 'package:apidash/widgets/widgets.dart';
import 'package:extended_text_field/extended_text_field.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mention_tag_text_field/mention_tag_text_field.dart';
import '../extensions/widget_tester_extensions.dart';
import '../test_consts.dart';
@ -41,6 +41,9 @@ void main() {
return null;
});
await openBoxes();
final flamante = rootBundle.load('google_fonts/OpenSans-Medium.ttf');
final fontLoader = FontLoader('OpenSans')..addFont(flamante);
await fontLoader.load();
});
group('Testing navRailIndexStateProvider', () {
@ -259,32 +262,44 @@ void main() {
'selectedIdEditStateProvider should not be null after rename button has been tapped',
(tester) async {
await tester.pumpWidget(
const ProviderScope(
ProviderScope(
child: MaterialApp(
home: Scaffold(
theme: ThemeData(
fontFamily: 'OpenSans',
),
home: const Scaffold(
body: CollectionPane(),
),
),
),
);
final collectionPane = tester.element(find.byType(CollectionPane));
final container = ProviderScope.containerOf(collectionPane);
var orig = container.read(selectedIdStateProvider);
expect(orig, isNotNull);
// Tap on the three dots to open the request card menu
await tester.tap(find.byType(RequestList));
await tester.pump();
await tester.tap(find.byType(RequestItem));
await tester.pump();
await tester.tap(find.byIcon(Icons.more_vert).first);
await tester.tap(find.byIcon(Icons.more_vert).at(1));
await tester.pumpAndSettle();
// Tap on the "Rename" option in the menu
await tester.tap(find.text('Rename'));
await tester.pumpAndSettle();
// Tap on the "Duplicate" option in the menu
var byType = find.text('Duplicate', findRichText: true);
expect(byType, findsOneWidget);
// Verify that the selectedIdEditStateProvider is not null
final collectionPane = tester.element(find.byType(CollectionPane));
final container = ProviderScope.containerOf(collectionPane);
expect(container.read(selectedIdEditStateProvider), isNotNull);
expect((container.read(selectedIdEditStateProvider)).runtimeType, String);
await tester.tap(byType);
await tester.pumpAndSettle();
// Screenshot
// await takeScreenshot();
var dupId = container.read(selectedIdStateProvider);
expect(dupId, isNotNull);
expect(dupId.runtimeType, String);
expect(dupId != orig, isTrue);
});
testWidgets(
@ -309,7 +324,7 @@ void main() {
await tester.pump();
await tester.tap(find.byType(RequestItem));
await tester.pump();
await tester.tap(find.byIcon(Icons.more_vert).first);
await tester.tap(find.byIcon(Icons.more_vert).at(1));
await tester.pumpAndSettle();
// Tap on the "Rename" option in the menu
@ -409,9 +424,10 @@ void main() {
// Add some data in URLTextField
Finder field = find.descendant(
of: find.byType(EnvURLField),
matching: find.byType(MentionTagTextField),
matching: find.byType(ExtendedTextField),
);
await tester.enterText(field, kTestUrl);
await tester.tap(field);
tester.testTextInput.enterText(kTestUrl);
await tester.pump();
// Tap on the "Send" button
@ -456,9 +472,10 @@ void main() {
// Add some data in URLTextField
Finder field = find.descendant(
of: find.byType(EnvURLField),
matching: find.byType(MentionTagTextField),
matching: find.byType(ExtendedTextField),
);
await tester.enterText(field, kTestUrl);
await tester.tap(field);
tester.testTextInput.enterText(kTestUrl);
await tester.pump();
// Tap on the "Send" button
@ -507,9 +524,10 @@ void main() {
// Add some data in URLTextField
Finder field = find.descendant(
of: find.byType(EnvURLField),
matching: find.byType(MentionTagTextField),
matching: find.byType(ExtendedTextField),
);
await tester.enterText(field, kTestUrl);
await tester.tap(field);
tester.testTextInput.enterText(kTestUrl);
await tester.pump();
// Tap on the "Send" button
@ -567,9 +585,10 @@ void main() {
// Add some data in URLTextField
Finder field = find.descendant(
of: find.byType(EnvURLField),
matching: find.byType(MentionTagTextField),
matching: find.byType(ExtendedTextField),
);
await tester.enterText(field, kTestUrl);
await tester.tap(field);
tester.testTextInput.enterText(kTestUrl);
await tester.pump();
// Tap on the "Send" button

View File

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:file_selector/file_selector.dart';
import 'package:apidash/widgets/drag_and_drop_area.dart';
void main() {
testWidgets('DragAndDropArea responds to file drop',
(WidgetTester tester) async {
bool fileDropped = false;
XFile? droppedFile;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: DragAndDropArea(
onFileDropped: (file) {
fileDropped = true;
droppedFile = file;
},
),
),
),
);
// Verify initial state
expect(find.text("Select or drop the file here"), findsOneWidget);
// Simulate dropping a file
final testFile = XFile('test.curl');
final dragAndDropArea =
tester.widget<DragAndDropArea>(find.byType(DragAndDropArea));
// Since we can't actually perform drag-and-drop in a unit test,
// we'll call the onDragDone callback directly
dragAndDropArea.onFileDropped?.call(testFile);
await tester.pump();
// Verify that the file was "dropped" and the callback was called
expect(fileDropped, isTrue);
expect(droppedFile, isNotNull);
expect(droppedFile?.path, 'test.curl');
});
}

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:apidash/widgets/menus.dart';
import 'package:apidash/widgets/menu_item_card.dart';
import 'package:apidash/consts.dart';
import '../test_consts.dart';