Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## Unreleased
## 8.3.0

Requires codelessly_api 6.2.0.

### Layout

Expand All @@ -14,7 +16,10 @@
`clamp`, `distance` (great-circle), `first`, `last`, `slice`, `take`,
`skip`, `reverse`, `range`, `indexOf`, `object`, `merge`, `set`, `keys`,
`values`, `append`, `setAt`, `insertAt`, `removeAt`, `move`, `flatten`,
`compact` and `thousands`.
`compact` and `thousands`; also `obj`, `concat`, `daysInMonth`,
`padLeft`, `padRight`, `substring`, `split`, `replace`, `coalesce`,
`formatNumber` and `matches` (regular expressions). `money` takes a digit
count.
- A map's own key wins over a built-in accessor of the same name
(`${rig.length}` reads the field).

Expand All @@ -24,12 +29,35 @@
- A link to the app's first screen gives it the link's parameters
(`${route.k}`).

### Rendering

- Linear gradients with `gradientRepeatEvery` repeat at a fixed pixel
period (`RepeatingGradientTransform`).
- Arc progress bars (gauges) with ticks, needle and zone bands
(`ArcProgressPainter`).
- Dialog canvases open as bottom sheets, adaptively (sheet below 600 px),
or aligned to a side or corner of the screen.

### Text fields

- `obscureText`, `enabled` and `readOnly` follow variables, conditions and
set-property actions.
- A one-line field with a set height stays one line (it can obscure its
text and Enter submits it) and fills its height through its decoration.
- Prefix and suffix icon constraints apply when set.

### Actions

- Delay actions wait as long as their `duration` formula says.

### Fixes

- Shadows without spread, offset or color no longer crash.
- Image fills apply their opacity once (they painted at its square).
- A text field's `errorText` template that comes out empty is no error.
- List items on the design canvas show their own item's text.
- An API document without a directory no longer fails to load
(`HttpApiData.directory` defaults to the root).

## 8.2.0

Expand Down
89 changes: 80 additions & 9 deletions lib/src/functions/functions_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,12 @@ class FunctionsRepository {
return runConfirm(context, action as ConfirmAction);
case ActionType.delay:
return Future<bool>.delayed(
Duration(milliseconds: (action as DelayAction).milliseconds),
Duration(
milliseconds: delayMilliseconds(
action as DelayAction,
ActionChain.scopedValuesOf(context),
),
),
() => true,
);
case ActionType.cloudData:
Expand All @@ -150,6 +155,23 @@ class FunctionsRepository {
}
}

/// How long [action] waits: its [DelayAction.duration] formula when that
/// gives a number, else its fixed [DelayAction.milliseconds].
static int delayMilliseconds(DelayAction action, ScopedValues scopedValues) {
final String? duration = action.duration;
if (duration == null || duration.trim().isEmpty) {
return action.milliseconds;
}
final num? value = num.tryParse(
PropertyValueDelegate.substituteVariables(
duration,
scopedValues: scopedValues,
nullSubstitutionMode: NullSubstitutionMode.emptyString,
).trim(),
);
return value == null || value < 0 ? action.milliseconds : value.round();
}

/// Runs a [CloudDataAction] and puts its result in its result variable
/// and in `${result}` for the next actions. A failure (a required field
/// left empty, a missing permission, ...) stops the chain.
Expand Down Expand Up @@ -571,6 +593,8 @@ class FunctionsRepository {
barrierDismissible: action.barrierDismissible,
barrierColor: action.barrierColor?.toFlutterColor(),
showCloseButton: action.showCloseButton,
presentation: action.presentation,
alignment: action.alignment.flutterAlignment ?? Alignment.center,
);
}

Expand All @@ -590,6 +614,8 @@ class FunctionsRepository {
bool barrierDismissible = true,
Color? barrierColor,
bool showCloseButton = false,
DialogPresentation presentation = DialogPresentation.dialog,
AlignmentGeometry alignment = Alignment.center,
}) async {
final ScopedValues scopedValues = ActionChain.scopedValuesOf(context);
final parsedParams = substituteVariablesInMap(params, scopedValues);
Expand Down Expand Up @@ -632,19 +658,17 @@ class FunctionsRepository {
// A chain triggered in the dialog can finish on this layout after the
// dialog closes (e.g. Navigate back, then a toast).
final List<BuildContext> openerHosts = ActionChainOpener.hostsFor(context);
final Object? result = await showDialog<Object?>(
context: context,
useRootNavigator: false,
final Object? result = await presentCanvas<Object?>(
context,
presentation: presentation,
alignment: alignment,
barrierDismissible: barrierDismissible,
barrierColor: barrierColor,
showCloseButton: showCloseButton,
routeSettings: RouteSettings(arguments: parsedParams),
builder: (context) => ActionChainOpener(
openerHosts: openerHosts,
child: CodelesslyDialogWidget(
showCloseButton: showCloseButton,
builder: (context) =>
CodelesslyWidget(codelessly: codelessly, layoutID: layoutId),
),
child: CodelesslyWidget(codelessly: codelessly, layoutID: layoutId),
),
);
if (context.mounted) {
Expand All @@ -657,6 +681,53 @@ class FunctionsRepository {
return result;
}

/// Opens what [builder] builds as a dialog at [alignment] or as a bottom
/// sheet, per [presentation] (adaptive: a sheet below 600 px wide), and
/// completes with the value it is closed with.
static Future<T?> presentCanvas<T>(
BuildContext context, {
required WidgetBuilder builder,
DialogPresentation presentation = DialogPresentation.dialog,
AlignmentGeometry alignment = Alignment.center,
bool barrierDismissible = true,
Color? barrierColor,
bool showCloseButton = false,
RouteSettings? routeSettings,
}) {
final bool asSheet = switch (presentation) {
DialogPresentation.dialog => false,
DialogPresentation.bottomSheet => true,
DialogPresentation.adaptive =>
(MediaQuery.maybeSizeOf(context)?.width ?? 1024) < 600,
};
Widget content(BuildContext context) => CodelesslyDialogWidget(
showCloseButton: showCloseButton,
alignment: asSheet ? Alignment.bottomCenter : alignment,
builder: builder,
);
if (asSheet) {
return showModalBottomSheet<T>(
context: context,
useRootNavigator: false,
isScrollControlled: true,
isDismissible: barrierDismissible,
barrierColor: barrierColor,
backgroundColor: Colors.transparent,
elevation: 0,
routeSettings: routeSettings,
builder: content,
);
}
return showDialog<T>(
context: context,
useRootNavigator: false,
barrierDismissible: barrierDismissible,
barrierColor: barrierColor,
routeSettings: routeSettings,
builder: content,
);
}

static void launchURL(BuildContext context, LinkAction action) {
final url = PropertyValueDelegate.substituteVariables(
action.url,
Expand Down
4 changes: 4 additions & 0 deletions lib/src/model/model_http_request.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ class HttpApiData extends PrivacyBase {
bool isDraft;
@DateTimeConverter()
final DateTime created;

/// Folder of the API in the editor. Missing in some saved APIs, so it
/// reads as the root folder rather than failing every API's load.
@JsonKey(defaultValue: '')
final String directory;

HttpApiData({
Expand Down
2 changes: 1 addition & 1 deletion lib/src/model/model_http_request.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading