mirror of
https://github.com/foss42/apidash.git
synced 2025-08-06 13:51:20 +08:00
feat: environment autocomplete fields
This commit is contained in:
71
lib/app.dart
71
lib/app.dart
@ -1,6 +1,7 @@
|
|||||||
// ignore_for_file: use_build_context_synchronously
|
// ignore_for_file: use_build_context_synchronously
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_portal/flutter_portal.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:window_manager/window_manager.dart' hide WindowCaption;
|
import 'package:window_manager/window_manager.dart' hide WindowCaption;
|
||||||
import 'widgets/widgets.dart' show WindowCaption;
|
import 'widgets/widgets.dart' show WindowCaption;
|
||||||
@ -106,41 +107,43 @@ class DashApp extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isDarkMode =
|
final isDarkMode =
|
||||||
ref.watch(settingsProvider.select((value) => value.isDark));
|
ref.watch(settingsProvider.select((value) => value.isDark));
|
||||||
return MaterialApp(
|
return Portal(
|
||||||
debugShowCheckedModeBanner: false,
|
child: MaterialApp(
|
||||||
theme: ThemeData(
|
debugShowCheckedModeBanner: false,
|
||||||
fontFamily: kFontFamily,
|
theme: ThemeData(
|
||||||
fontFamilyFallback: kFontFamilyFallback,
|
fontFamily: kFontFamily,
|
||||||
colorSchemeSeed: kColorSchemeSeed,
|
fontFamilyFallback: kFontFamilyFallback,
|
||||||
useMaterial3: true,
|
colorSchemeSeed: kColorSchemeSeed,
|
||||||
brightness: Brightness.light,
|
useMaterial3: true,
|
||||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
brightness: Brightness.light,
|
||||||
),
|
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||||
darkTheme: ThemeData(
|
),
|
||||||
fontFamily: kFontFamily,
|
darkTheme: ThemeData(
|
||||||
fontFamilyFallback: kFontFamilyFallback,
|
fontFamily: kFontFamily,
|
||||||
colorSchemeSeed: kColorSchemeSeed,
|
fontFamilyFallback: kFontFamilyFallback,
|
||||||
useMaterial3: true,
|
colorSchemeSeed: kColorSchemeSeed,
|
||||||
brightness: Brightness.dark,
|
useMaterial3: true,
|
||||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
brightness: Brightness.dark,
|
||||||
),
|
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||||
themeMode: isDarkMode ? ThemeMode.dark : ThemeMode.light,
|
),
|
||||||
home: Stack(
|
themeMode: isDarkMode ? ThemeMode.dark : ThemeMode.light,
|
||||||
children: [
|
home: Stack(
|
||||||
!kIsLinux && !kIsMobile
|
children: [
|
||||||
? const App()
|
!kIsLinux && !kIsMobile
|
||||||
: context.isMediumWindow
|
? const App()
|
||||||
? const MobileDashboard()
|
: context.isMediumWindow
|
||||||
: const Dashboard(),
|
? const MobileDashboard()
|
||||||
if (kIsWindows)
|
: const Dashboard(),
|
||||||
SizedBox(
|
if (kIsWindows)
|
||||||
height: 29,
|
SizedBox(
|
||||||
child: WindowCaption(
|
height: 29,
|
||||||
backgroundColor: Colors.transparent,
|
child: WindowCaption(
|
||||||
brightness: isDarkMode ? Brightness.dark : Brightness.light,
|
backgroundColor: Colors.transparent,
|
||||||
|
brightness: isDarkMode ? Brightness.dark : Brightness.light,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -66,6 +66,7 @@ const kFormDataButtonLabelTextStyle = TextStyle(
|
|||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const kBorderRadius4 = BorderRadius.all(Radius.circular(4));
|
||||||
const kBorderRadius8 = BorderRadius.all(Radius.circular(8));
|
const kBorderRadius8 = BorderRadius.all(Radius.circular(8));
|
||||||
final kBorderRadius10 = BorderRadius.circular(10);
|
final kBorderRadius10 = BorderRadius.circular(10);
|
||||||
const kBorderRadius12 = BorderRadius.all(Radius.circular(12));
|
const kBorderRadius12 = BorderRadius.all(Radius.circular(12));
|
||||||
@ -286,6 +287,8 @@ enum FormDataType { text, file }
|
|||||||
|
|
||||||
enum EnvironmentVariableType { variable, secret }
|
enum EnvironmentVariableType { variable, secret }
|
||||||
|
|
||||||
|
final kEnvVarRegEx = RegExp(r'{{(.*?)}}');
|
||||||
|
|
||||||
const kSupportedUriSchemes = ["https", "http"];
|
const kSupportedUriSchemes = ["https", "http"];
|
||||||
const kDefaultUriScheme = "https";
|
const kDefaultUriScheme = "https";
|
||||||
const kMethodsWithBody = [
|
const kMethodsWithBody = [
|
||||||
|
@ -42,3 +42,27 @@ const kEnvironmentVariableEmptyModel =
|
|||||||
EnvironmentVariableModel(key: "", value: "");
|
EnvironmentVariableModel(key: "", value: "");
|
||||||
const kEnvironmentSecretEmptyModel = EnvironmentVariableModel(
|
const kEnvironmentSecretEmptyModel = EnvironmentVariableModel(
|
||||||
key: "", value: "", type: EnvironmentVariableType.secret);
|
key: "", value: "", type: EnvironmentVariableType.secret);
|
||||||
|
|
||||||
|
class EnvironmentVariableSuggestion {
|
||||||
|
final String environmentId;
|
||||||
|
final EnvironmentVariableModel variable;
|
||||||
|
final bool isUnknown;
|
||||||
|
|
||||||
|
const EnvironmentVariableSuggestion({
|
||||||
|
required this.environmentId,
|
||||||
|
required this.variable,
|
||||||
|
this.isUnknown = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
EnvironmentVariableSuggestion copyWith({
|
||||||
|
String? environmentId,
|
||||||
|
EnvironmentVariableModel? variable,
|
||||||
|
bool? isUnknown,
|
||||||
|
}) {
|
||||||
|
return EnvironmentVariableSuggestion(
|
||||||
|
environmentId: environmentId ?? this.environmentId,
|
||||||
|
variable: variable ?? this.variable,
|
||||||
|
isUnknown: isUnknown ?? this.isUnknown,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6,6 +6,7 @@ final mobileScaffoldKeyStateProvider = StateProvider<GlobalKey<ScaffoldState>>(
|
|||||||
final leftDrawerStateProvider = StateProvider<bool>((ref) => false);
|
final leftDrawerStateProvider = StateProvider<bool>((ref) => false);
|
||||||
final navRailIndexStateProvider = StateProvider<int>((ref) => 0);
|
final navRailIndexStateProvider = StateProvider<int>((ref) => 0);
|
||||||
final selectedIdEditStateProvider = StateProvider<String?>((ref) => null);
|
final selectedIdEditStateProvider = StateProvider<String?>((ref) => null);
|
||||||
|
final environmentFieldEditStateProvider = StateProvider<String?>((ref) => null);
|
||||||
final codePaneVisibleStateProvider = StateProvider<bool>((ref) => false);
|
final codePaneVisibleStateProvider = StateProvider<bool>((ref) => false);
|
||||||
final saveDataStateProvider = StateProvider<bool>((ref) => false);
|
final saveDataStateProvider = StateProvider<bool>((ref) => false);
|
||||||
final clearDataStateProvider = StateProvider<bool>((ref) => false);
|
final clearDataStateProvider = StateProvider<bool>((ref) => false);
|
||||||
|
200
lib/screens/common/environment_autocomplete.dart
Normal file
200
lib/screens/common/environment_autocomplete.dart
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
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/environment_model.dart';
|
||||||
|
import 'package:apidash/providers/providers.dart';
|
||||||
|
import 'package:apidash/utils/envvar_utils.dart';
|
||||||
|
import 'package:apidash/widgets/widgets.dart';
|
||||||
|
|
||||||
|
class EnvironmentAutocompleteField extends HookConsumerWidget {
|
||||||
|
const EnvironmentAutocompleteField({
|
||||||
|
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 EnvironmentAutocompleteFieldBase(
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EnvironmentVariableSpan extends HookConsumerWidget {
|
||||||
|
const EnvironmentVariableSpan({
|
||||||
|
super.key,
|
||||||
|
required this.suggestion,
|
||||||
|
});
|
||||||
|
|
||||||
|
final EnvironmentVariableSuggestion suggestion;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final environments = ref.watch(environmentsStateNotifierProvider);
|
||||||
|
final envMap = ref.watch(availableEnvironmentVariablesStateProvider);
|
||||||
|
final activeEnvironmentId = ref.watch(activeEnvironmentIdStateProvider);
|
||||||
|
|
||||||
|
final currentSuggestion =
|
||||||
|
getCurrentVariableStatus(suggestion, envMap, activeEnvironmentId);
|
||||||
|
|
||||||
|
final showPopover = useState(false);
|
||||||
|
|
||||||
|
final isMissingVariable = currentSuggestion.isUnknown;
|
||||||
|
final String scope = isMissingVariable
|
||||||
|
? 'unknown'
|
||||||
|
: getEnvironmentTitle(
|
||||||
|
environments?[currentSuggestion.environmentId]?.name);
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
var text = Text(
|
||||||
|
'{{${currentSuggestion.variable.key}}}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: isMissingVariable ? colorScheme.error : colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.w600),
|
||||||
|
);
|
||||||
|
|
||||||
|
return PortalTarget(
|
||||||
|
visible: showPopover.value,
|
||||||
|
portalFollower: MouseRegion(
|
||||||
|
onEnter: (_) {
|
||||||
|
showPopover.value = true;
|
||||||
|
},
|
||||||
|
onExit: (_) {
|
||||||
|
showPopover.value = false;
|
||||||
|
},
|
||||||
|
child:
|
||||||
|
EnvironmentPopoverCard(suggestion: currentSuggestion, scope: scope),
|
||||||
|
),
|
||||||
|
anchor: const Aligned(
|
||||||
|
follower: Alignment.bottomCenter,
|
||||||
|
target: Alignment.topCenter,
|
||||||
|
backup: Aligned(
|
||||||
|
follower: Alignment.topCenter,
|
||||||
|
target: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: kIsMobile
|
||||||
|
? TapRegion(
|
||||||
|
onTapInside: (_) {
|
||||||
|
showPopover.value = true;
|
||||||
|
},
|
||||||
|
onTapOutside: (_) {
|
||||||
|
showPopover.value = false;
|
||||||
|
},
|
||||||
|
child: text,
|
||||||
|
)
|
||||||
|
: MouseRegion(
|
||||||
|
onEnter: (_) {
|
||||||
|
showPopover.value = true;
|
||||||
|
},
|
||||||
|
onExit: (_) {
|
||||||
|
showPopover.value = false;
|
||||||
|
},
|
||||||
|
child: text,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EnvironmentPopoverCard extends StatelessWidget {
|
||||||
|
const EnvironmentPopoverCard({
|
||||||
|
super.key,
|
||||||
|
required this.suggestion,
|
||||||
|
required this.scope,
|
||||||
|
});
|
||||||
|
|
||||||
|
final EnvironmentVariableSuggestion suggestion;
|
||||||
|
final String scope;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
type: MaterialType.card,
|
||||||
|
borderRadius: kBorderRadius8,
|
||||||
|
elevation: 8,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 200),
|
||||||
|
child: Ink(
|
||||||
|
padding: kP8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
borderRadius: kBorderRadius8,
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
EnvironmentIndicator(suggestion: suggestion),
|
||||||
|
kHSpacer10,
|
||||||
|
Text(suggestion.variable.key),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
kVSpacer5,
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'VALUE',
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
kHSpacer10,
|
||||||
|
Text(suggestion.variable.value),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
kVSpacer5,
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'SCOPE',
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
kHSpacer10,
|
||||||
|
Text(scope),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
89
lib/screens/common/environment_textfields.dart
Normal file
89
lib/screens/common/environment_textfields.dart
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:apidash/consts.dart';
|
||||||
|
import 'environment_autocomplete.dart';
|
||||||
|
|
||||||
|
class EnvURLField extends StatelessWidget {
|
||||||
|
const EnvURLField({
|
||||||
|
super.key,
|
||||||
|
required this.selectedId,
|
||||||
|
this.initialValue,
|
||||||
|
this.onChanged,
|
||||||
|
this.onFieldSubmitted,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String selectedId;
|
||||||
|
final String? initialValue;
|
||||||
|
final void Function(String)? onChanged;
|
||||||
|
final void Function(String)? onFieldSubmitted;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return EnvironmentAutocompleteField(
|
||||||
|
keyId: "url-$selectedId",
|
||||||
|
initialValue: initialValue,
|
||||||
|
style: kCodeStyle,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: kHintTextUrlCard,
|
||||||
|
hintStyle: kCodeStyle.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.outline.withOpacity(
|
||||||
|
kHintOpacity,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
onChanged: onChanged,
|
||||||
|
onFieldSubmitted: onFieldSubmitted,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EnvCellField extends StatelessWidget {
|
||||||
|
const EnvCellField({
|
||||||
|
super.key,
|
||||||
|
required this.keyId,
|
||||||
|
this.initialValue,
|
||||||
|
this.hintText,
|
||||||
|
this.onChanged,
|
||||||
|
this.colorScheme,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String keyId;
|
||||||
|
final String? initialValue;
|
||||||
|
final String? hintText;
|
||||||
|
final void Function(String)? onChanged;
|
||||||
|
final ColorScheme? colorScheme;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var clrScheme = colorScheme ?? Theme.of(context).colorScheme;
|
||||||
|
return EnvironmentAutocompleteField(
|
||||||
|
keyId: keyId,
|
||||||
|
initialValue: initialValue,
|
||||||
|
style: kCodeStyle.copyWith(
|
||||||
|
color: clrScheme.onSurface,
|
||||||
|
),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintStyle: kCodeStyle.copyWith(
|
||||||
|
color: clrScheme.outline.withOpacity(
|
||||||
|
kHintOpacity,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
hintText: hintText,
|
||||||
|
contentPadding: const EdgeInsets.only(bottom: 12),
|
||||||
|
focusedBorder: UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: clrScheme.primary.withOpacity(
|
||||||
|
kHintOpacity,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
enabledBorder: UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: clrScheme.surfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: onChanged,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -86,6 +86,9 @@ class EnvironmentDropdown extends ConsumerWidget {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
ref.read(activeEnvironmentIdStateProvider.notifier).state =
|
ref.read(activeEnvironmentIdStateProvider.notifier).state =
|
||||||
value?.id;
|
value?.id;
|
||||||
|
ref
|
||||||
|
.read(settingsProvider.notifier)
|
||||||
|
.update(activeEnvironmentId: value?.id);
|
||||||
ref.read(hasUnsavedChangesProvider.notifier).state = true;
|
ref.read(hasUnsavedChangesProvider.notifier).state = true;
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
@ -6,6 +6,7 @@ import 'package:apidash/providers/providers.dart';
|
|||||||
import 'package:apidash/widgets/widgets.dart';
|
import 'package:apidash/widgets/widgets.dart';
|
||||||
import 'package:apidash/models/models.dart';
|
import 'package:apidash/models/models.dart';
|
||||||
import 'package:apidash/consts.dart';
|
import 'package:apidash/consts.dart';
|
||||||
|
import 'package:apidash/screens/common/environment_textfields.dart';
|
||||||
|
|
||||||
class EditRequestHeaders extends ConsumerStatefulWidget {
|
class EditRequestHeaders extends ConsumerStatefulWidget {
|
||||||
const EditRequestHeaders({super.key});
|
const EditRequestHeaders({super.key});
|
||||||
@ -129,7 +130,7 @@ class EditRequestHeadersState extends ConsumerState<EditRequestHeaders> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
DataCell(
|
DataCell(
|
||||||
CellField(
|
EnvCellField(
|
||||||
keyId: "$selectedId-$index-headers-v-$seed",
|
keyId: "$selectedId-$index-headers-v-$seed",
|
||||||
initialValue: headerRows[index].value,
|
initialValue: headerRows[index].value,
|
||||||
hintText: kHintAddValue,
|
hintText: kHintAddValue,
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:data_table_2/data_table_2.dart';
|
||||||
import 'package:apidash/providers/providers.dart';
|
import 'package:apidash/providers/providers.dart';
|
||||||
import 'package:apidash/widgets/widgets.dart';
|
import 'package:apidash/widgets/widgets.dart';
|
||||||
import 'package:apidash/models/models.dart';
|
import 'package:apidash/models/models.dart';
|
||||||
import 'package:apidash/consts.dart';
|
import 'package:apidash/consts.dart';
|
||||||
import 'package:data_table_2/data_table_2.dart';
|
import 'package:apidash/screens/common/environment_textfields.dart';
|
||||||
|
|
||||||
class EditRequestURLParams extends ConsumerStatefulWidget {
|
class EditRequestURLParams extends ConsumerStatefulWidget {
|
||||||
const EditRequestURLParams({super.key});
|
const EditRequestURLParams({super.key});
|
||||||
@ -103,7 +104,7 @@ class EditRequestURLParamsState extends ConsumerState<EditRequestURLParams> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
DataCell(
|
DataCell(
|
||||||
CellField(
|
EnvCellField(
|
||||||
keyId: "$selectedId-$index-params-k-$seed",
|
keyId: "$selectedId-$index-params-k-$seed",
|
||||||
initialValue: paramRows[index].name,
|
initialValue: paramRows[index].name,
|
||||||
hintText: kHintAddURLParam,
|
hintText: kHintAddURLParam,
|
||||||
@ -129,7 +130,7 @@ class EditRequestURLParamsState extends ConsumerState<EditRequestURLParams> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
DataCell(
|
DataCell(
|
||||||
CellField(
|
EnvCellField(
|
||||||
keyId: "$selectedId-$index-params-v-$seed",
|
keyId: "$selectedId-$index-params-v-$seed",
|
||||||
initialValue: paramRows[index].value,
|
initialValue: paramRows[index].value,
|
||||||
hintText: kHintAddValue,
|
hintText: kHintAddValue,
|
||||||
|
@ -4,6 +4,7 @@ import 'package:apidash/providers/providers.dart';
|
|||||||
import 'package:apidash/widgets/widgets.dart';
|
import 'package:apidash/widgets/widgets.dart';
|
||||||
import 'package:apidash/consts.dart';
|
import 'package:apidash/consts.dart';
|
||||||
import 'package:apidash/extensions/extensions.dart';
|
import 'package:apidash/extensions/extensions.dart';
|
||||||
|
import '../../common/environment_textfields.dart';
|
||||||
|
|
||||||
class EditorPaneRequestURLCard extends StatelessWidget {
|
class EditorPaneRequestURLCard extends StatelessWidget {
|
||||||
const EditorPaneRequestURLCard({super.key});
|
const EditorPaneRequestURLCard({super.key});
|
||||||
@ -82,7 +83,7 @@ class URLTextField extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final selectedId = ref.watch(selectedIdStateProvider);
|
final selectedId = ref.watch(selectedIdStateProvider);
|
||||||
return URLField(
|
return EnvURLField(
|
||||||
selectedId: selectedId!,
|
selectedId: selectedId!,
|
||||||
initialValue: ref
|
initialValue: ref
|
||||||
.read(collectionStateNotifierProvider.notifier)
|
.read(collectionStateNotifierProvider.notifier)
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import 'package:apidash/consts.dart';
|
import 'package:apidash/consts.dart';
|
||||||
import 'package:apidash/models/models.dart';
|
import 'package:apidash/models/models.dart';
|
||||||
|
import 'package:apidash/screens/common/environment_autocomplete.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
String getEnvironmentTitle(String? name) {
|
String getEnvironmentTitle(String? name) {
|
||||||
if (name == null || name.trim() == "") {
|
if (name == null || name.trim() == "") {
|
||||||
@ -53,8 +56,7 @@ String? substituteVariables(
|
|||||||
combinedMap[variable.key] = variable.value;
|
combinedMap[variable.key] = variable.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
final regex = RegExp(r'{{(.*?)}}');
|
String result = input.replaceAllMapped(kEnvVarRegEx, (match) {
|
||||||
String result = input.replaceAllMapped(regex, (match) {
|
|
||||||
final key = match.group(1)?.trim() ?? '';
|
final key = match.group(1)?.trim() ?? '';
|
||||||
return combinedMap[key] ?? '';
|
return combinedMap[key] ?? '';
|
||||||
});
|
});
|
||||||
@ -74,14 +76,124 @@ HttpRequestModel substituteHttpRequestModel(
|
|||||||
)!,
|
)!,
|
||||||
headers: httpRequestModel.headers?.map((header) {
|
headers: httpRequestModel.headers?.map((header) {
|
||||||
return header.copyWith(
|
return header.copyWith(
|
||||||
|
name:
|
||||||
|
substituteVariables(header.name, envMap, activeEnvironmentId) ?? "",
|
||||||
value: substituteVariables(header.value, envMap, activeEnvironmentId),
|
value: substituteVariables(header.value, envMap, activeEnvironmentId),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
params: httpRequestModel.params?.map((param) {
|
params: httpRequestModel.params?.map((param) {
|
||||||
return param.copyWith(
|
return param.copyWith(
|
||||||
|
name:
|
||||||
|
substituteVariables(param.name, envMap, activeEnvironmentId) ?? "",
|
||||||
value: substituteVariables(param.value, envMap, activeEnvironmentId),
|
value: substituteVariables(param.value, envMap, activeEnvironmentId),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
);
|
);
|
||||||
return newRequestModel;
|
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,
|
||||||
|
EnvironmentVariableSpan(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(
|
||||||
|
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)) &&
|
||||||
|
!addedVariableKeys.contains(variable.key)) {
|
||||||
|
suggestions.add(EnvironmentVariableSuggestion(
|
||||||
|
environmentId: activeEnvironmentId, variable: variable));
|
||||||
|
addedVariableKeys.add(variable.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
envMap[kGlobalEnvironmentId]?.forEach((variable) {
|
||||||
|
if ((query!.isEmpty || variable.key.contains(query)) &&
|
||||||
|
!addedVariableKeys.contains(variable.key)) {
|
||||||
|
suggestions.add(EnvironmentVariableSuggestion(
|
||||||
|
environmentId: kGlobalEnvironmentId, variable: variable));
|
||||||
|
addedVariableKeys.add(variable.key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
261
lib/widgets/environment_field.dart
Normal file
261
lib/widgets/environment_field.dart
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
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/consts.dart';
|
||||||
|
import '../screens/common/environment_autocomplete.dart';
|
||||||
|
|
||||||
|
class EnvironmentAutocompleteFieldBase extends StatefulHookWidget {
|
||||||
|
const EnvironmentAutocompleteFieldBase({
|
||||||
|
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<EnvironmentAutocompleteFieldBase> createState() =>
|
||||||
|
_EnvironmentAutocompleteFieldBaseState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EnvironmentAutocompleteFieldBaseState
|
||||||
|
extends State<EnvironmentAutocompleteFieldBase> {
|
||||||
|
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: EnvironmentSuggestionsMenu(
|
||||||
|
mentionController: controller,
|
||||||
|
suggestions: widget.suggestions,
|
||||||
|
onSelect: (suggestion) {
|
||||||
|
controller.addMention(
|
||||||
|
label: '{${suggestion.variable.key}}}',
|
||||||
|
data: suggestion,
|
||||||
|
stylingWidget: EnvironmentVariableSpan(suggestion: suggestion));
|
||||||
|
widget.onChanged?.call(controller.text);
|
||||||
|
widget.onMentionValueChanged.call(null);
|
||||||
|
isSuggestionsVisible.value = false;
|
||||||
|
var mentionsCharacters =
|
||||||
|
controller.mentions.fold<int>(0, (previousValue, element) {
|
||||||
|
return previousValue + element.variable.key.length + 4 as int;
|
||||||
|
});
|
||||||
|
controller.selection = TextSelection.collapsed(
|
||||||
|
offset: controller.text.length +
|
||||||
|
controller.mentions.length -
|
||||||
|
mentionsCharacters,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
anchor: const Aligned(
|
||||||
|
follower: Alignment.topLeft,
|
||||||
|
target: Alignment.bottomLeft,
|
||||||
|
backup: Aligned(
|
||||||
|
follower: Alignment.bottomLeft,
|
||||||
|
target: Alignment.topLeft,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: EnvironmentMentionField(
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EnvironmentMentionField extends StatelessWidget {
|
||||||
|
const EnvironmentMentionField({
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
void onMention(String? value) {
|
||||||
|
onMentionValueChanged.call(value);
|
||||||
|
if (value != null) {
|
||||||
|
isSuggestionsVisible.value = true;
|
||||||
|
} else {
|
||||||
|
isSuggestionsVisible.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MentionTagTextFormField(
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
onFieldSubmitted: (value) {
|
||||||
|
onFieldSubmitted?.call(controller.text);
|
||||||
|
isSuggestionsVisible.value = false;
|
||||||
|
},
|
||||||
|
decoration: decoration,
|
||||||
|
mentionTagDecoration: const MentionTagDecoration(
|
||||||
|
mentionStart: ['{'],
|
||||||
|
mentionBreak:
|
||||||
|
" ", // This is a workaround for the exception but adds a space after the mention
|
||||||
|
maxWords: 1,
|
||||||
|
allowDecrement: false,
|
||||||
|
allowEmbedding: true,
|
||||||
|
showMentionStartSymbol: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EnvironmentSuggestionsMenu extends StatelessWidget {
|
||||||
|
const EnvironmentSuggestionsMenu({
|
||||||
|
super.key,
|
||||||
|
required this.mentionController,
|
||||||
|
required this.suggestions,
|
||||||
|
this.onSelect,
|
||||||
|
});
|
||||||
|
|
||||||
|
final MentionTagTextEditingController mentionController;
|
||||||
|
final List<EnvironmentVariableSuggestion>? suggestions;
|
||||||
|
final Function(EnvironmentVariableSuggestion)? onSelect;
|
||||||
|
|
||||||
|
@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: const BoxConstraints(maxHeight: 200),
|
||||||
|
child: Ink(
|
||||||
|
width: 300,
|
||||||
|
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: (context, index) {
|
||||||
|
final suggestion = suggestions![index];
|
||||||
|
return ListTile(
|
||||||
|
dense: true,
|
||||||
|
leading: EnvironmentIndicator(suggestion: suggestion),
|
||||||
|
title: Text(suggestion.variable.key),
|
||||||
|
subtitle: Text(suggestion.variable.value),
|
||||||
|
onTap: () {
|
||||||
|
onSelect?.call(suggestions![index]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EnvironmentIndicator extends StatelessWidget {
|
||||||
|
const EnvironmentIndicator({super.key, required this.suggestion});
|
||||||
|
|
||||||
|
final EnvironmentVariableSuggestion suggestion;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isUnknown = suggestion.isUnknown;
|
||||||
|
final isGlobal = suggestion.environmentId == kGlobalEnvironmentId;
|
||||||
|
return Container(
|
||||||
|
padding: kP4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isUnknown
|
||||||
|
? Theme.of(context).colorScheme.errorContainer
|
||||||
|
: isGlobal
|
||||||
|
? Theme.of(context).colorScheme.secondaryContainer
|
||||||
|
: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
borderRadius: kBorderRadius4,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
isUnknown
|
||||||
|
? Icons.block
|
||||||
|
: isGlobal
|
||||||
|
? Icons.public
|
||||||
|
: Icons.computer,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ export 'codegen_previewer.dart';
|
|||||||
export 'dropdowns.dart';
|
export 'dropdowns.dart';
|
||||||
export 'editor_json.dart';
|
export 'editor_json.dart';
|
||||||
export 'editor.dart';
|
export 'editor.dart';
|
||||||
|
export 'environment_field.dart';
|
||||||
export 'error_message.dart';
|
export 'error_message.dart';
|
||||||
export 'headerfield.dart';
|
export 'headerfield.dart';
|
||||||
export 'intro_message.dart';
|
export 'intro_message.dart';
|
||||||
|
18
pubspec.lock
18
pubspec.lock
@ -470,6 +470,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.23"
|
version: "0.6.23"
|
||||||
|
flutter_portal:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_portal
|
||||||
|
sha256: "4601b3dc24f385b3761721bd852a3f6c09cddd4e943dd184ed58ee1f43006257"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.4"
|
||||||
flutter_riverpod:
|
flutter_riverpod:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -809,6 +817,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.0"
|
version: "0.8.0"
|
||||||
|
mention_tag_text_field:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: mention_tag_text_field
|
||||||
|
sha256: b0e831f5fc8ca942a835916fd084e6f5054226715affc740c613cd320004b8a7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.4"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1520,5 +1536,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.3.0 <4.0.0"
|
dart: ">=3.3.4 <4.0.0"
|
||||||
flutter: ">=3.19.2"
|
flutter: ">=3.19.2"
|
||||||
|
@ -62,6 +62,8 @@ dependencies:
|
|||||||
file_selector: ^1.0.3
|
file_selector: ^1.0.3
|
||||||
hooks_riverpod: ^2.5.1
|
hooks_riverpod: ^2.5.1
|
||||||
flutter_hooks: ^0.20.5
|
flutter_hooks: ^0.20.5
|
||||||
|
flutter_portal: ^1.1.4
|
||||||
|
mention_tag_text_field: ^0.0.4
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
web: ^0.5.0
|
web: ^0.5.0
|
||||||
|
Reference in New Issue
Block a user