mirror of
https://github.com/foss42/apidash.git
synced 2025-08-06 13:51:20 +08:00
Merge branch 'main' into add-feature-history
This commit is contained in:
18
lib/app.dart
18
lib/app.dart
@ -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: [
|
||||
|
@ -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";
|
||||
|
42
lib/importer/curl/curl.dart
Normal file
42
lib/importer/curl/curl.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
17
lib/importer/importer.dart
Normal file
17
lib/importer/importer.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
|
27
lib/screens/common_widgets/env_regexp_span_builder.dart
Normal file
27
lib/screens/common_widgets/env_regexp_span_builder.dart
Normal 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)),
|
||||
);
|
||||
}
|
||||
}
|
113
lib/screens/common_widgets/env_trigger_field.dart
Normal file
113
lib/screens/common_widgets/env_trigger_field.dart
Normal 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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
66
lib/screens/common_widgets/env_trigger_options.dart
Normal file
66
lib/screens/common_widgets/env_trigger_options.dart
Normal 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),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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();
|
||||
},
|
||||
|
@ -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();
|
||||
|
@ -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"));
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -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 {
|
||||
|
39
lib/widgets/dialog_import.dart
Normal file
39
lib/widgets/dialog_import.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
94
lib/widgets/drag_and_drop_area.dart
Normal file
94
lib/widgets/drag_and_drop_area.dart
Normal 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")),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
51
lib/widgets/dropdown_import_format.dart
Normal file
51
lib/widgets/dropdown_import_format.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
44
lib/widgets/menu_sidebar_top.dart
Normal file
44
lib/widgets/menu_sidebar_top.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
133
pubspec.lock
133
pubspec.lock
@ -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"
|
||||
|
12
pubspec.yaml
12
pubspec.yaml
@ -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
|
||||
|
@ -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
|
||||
|
44
test/widgets/drag_and_drop_area_test.dart
Normal file
44
test/widgets/drag_and_drop_area_test.dart
Normal 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');
|
||||
});
|
||||
}
|
@ -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';
|
||||
|
Reference in New Issue
Block a user