feat: offline translations + logic corrections cumulative (closes #42 #43 #47 #49 #52 #53 #64 #72)#76
Open
ankitDhwani wants to merge 32 commits into
Open
Conversation
… code Consolidate repeated patterns into single sources of truth so future changes are made in one place instead of 10-20. New helpers: - utils/date_helpers.dart — parseDateTime, parseTime, formatDurationSeconds shared by date/datetime/duration fields and form_builder patch normaliser - utils/frappe_json_utils.dart — parseBool: canonical 0/1/'true'/bool coercion for every model fromJson - utils/sql_row_utils.dart — retryCountFrom, lastAttemptAtFrom, utcMillisFrom, parseEnumByName for sqflite row factories - ui/widgets/fields/field_helpers.dart — requiredValidator, baseFieldDecoration, fieldErrorText shared by every Form*Field - ui/widgets/screen_helpers.dart — showStatusSnackBar + showConfirmDialog replace ad-hoc SnackBar/Dialog construction across screens Schema/query additions: - system_columns: systemSyncMetadataColumnNames + linkCompanionColumnDDL consumed by PayloadAssembler, PayloadSerializer, parent/child schema builders and runtime migration so strip rules cannot diverge - table_name: stripDocsPrefix inverse of normalizeDoctypeTableName - query_result: QueryResult.ofRows derives hasMore + returnedCount for online/offline parity in UnifiedResolver No behaviour change. flutter analyze + dart format clean across the package.
… UX polish - DoctypeService.list: guard non-List Frappe message shape → treat as empty page - RestHelper.get/getPublic: expose per-call timeout and maxRetries overrides so boot/splash callers can fast-fail instead of burning the default 30s × 3-retry budget - PermissionService.syncFromApi: accept timeout param; remove silent catch so callers own error handling - MobileHomeScreen: suppress full-screen spinner on background syncComplete$ reloads — only show it on first paint when the list is empty - FieldStyle: default showLabel/showDescription to false - Tests: Cursor.start round-trip, initial-sync limit_start advance vs incremental modified-filter path, stall guard cursor pre-seeding
- FrappeSDK._runUpgradeClosurePull: call ensureSchemaForClosure before the closure pull so per-doctype mirror tables exist (closes the "no such table" hole that previously required SNF's runSnfPostSdkSync to re-pull) - FrappeSDK._initialMetaAndDataSync: bracket the boot pull with isInitialSync=true so host UI subscribed to state$ can render a syncing indicator; reset on every exit path - FrappeSDK: surface boot failures via SyncStateNotifier.lastError (code: 'network'|'permissions'), expose retryInitialMetaAndDataSync(), clear lastError on logout - parent/child schema DDL: CREATE TABLE/INDEX IF NOT EXISTS so SNF's belt-and-suspenders ensureSchemaForClosure and the SDK's eager pull can re-enter safely after a partial failure - PullEngine: optional SchemaReconcilerFn wired to OfflineRepository.reconcileParentTableForMeta so ALTER ADD COLUMN runs before applying a page (closes the meta-cache race) - PullEngine stall guard now scoped to incremental (complete=true); initial-sync uses limit_start offset which always advances - PushEngine: optional PayloadTransformerFn between assembly and HTTP dispatch (e.g. host auto-submit on sync); exception-safe - FrappeFormBuilder: onValidationFailed callback so parents can stop loading indicators when submit is rejected - FrappeFormBuilder: ??=-inject linkOptionService / linkFieldCoordinator on host-supplied customFieldFactory so Link pickers aren't half-configured - SyncStateNotifier: recordLastError / clearLastError bypass copyWith (cannot express "set to null") - SDK barrel: export PayloadTransformerFn + ChildTableFormBuilder - Tests: PayloadTransformerFn smoke; onValidationFailed fires on invalid submit and not on valid
…logic_correction
Conflict resolution favoured HEAD (om/logic_correction): kept the
`stripDocsPrefix` helper, the `tableNameFor` docblock, `_assertInitialized`,
`_buildPullableDoctypes`, the `payloadTransformer` parameter wiring, and the
`closure_result.dart` import. The incoming `3cbb9e6` ("Port of
om/logic_correction fixes to om/offline_improvement") was an earlier
snapshot of the same work, so HEAD's refactored versions are the canonical
copy. Also picked up the `.gitignore` trailing-newline strip from the
incoming branch.
…-mobile-sdk into om/logic_correction
# Conflicts: # lib/src/database/app_database.dart # lib/src/database/daos/outbox_dao.dart # lib/src/screens/mobile_home_screen.dart # lib/src/ui/document_list_screen.dart # lib/src/ui/widgets/form_builder.dart
Combines the offline-push correctness fixes with logic_correction's
payload-transformer / DDL-helper refactors. Conflict resolutions:
- push_engine.dart / sync_engine_builder.dart: kept BOTH logic's
PayloadTransformerFn (field + re-export) AND offline's deadlock retry
(DeadlockError, isDeadlockApiException, _withJitter, _dispatchUnits).
- system_columns.dart: linkCompanionColumnDDL now quotes the column
("name__is_local") — keeps logic's single-source helper while honoring
offline's "quote identifiers in DDL" fix (parent/child schema use it).
- offline_repository.dart: kept offline's per-iteration try/catch +
_reconcileParentTableSchema healing, using logic's _executeDDL helper.
- form_builder.dart (from prior develop merge): submit path keeps the
hidden-field strip; _buildCompleteFormData retained for dirty detection.
Full suite green (1304 tests); analyzer clean.
…cripts/) These were swept in by `git add -A` during the merges; they are local developer tooling, not part of the package. Untracked and gitignored.
…mable pull, pre-flight manifest - dhwani-ris#43: PullApply matches a pulled parent by mobile_uuid when server_name is not yet stamped locally, preventing duplicate rows during an in-flight push INSERT; reconcile (no false conflict) when local server_name is NULL. Extend the hasActivePushFor defer to SyncService pull paths (parity with PullEngine) via SyncStatus.deferredActivePush. - dhwani-ris#47: SyncMode {full, pushOnly, pullOnly} on SyncController.syncNow (default full = existing behavior). - dhwani-ris#64: checkpoint the PullEngine cursor (complete:false) after every applied page so an app-kill mid initial-sync resumes from the last page, not page 0. - dhwani-ris#49: /sync_details pre-flight client (DoctypeService.getSyncDetails) + pure doctypesToSkip; wired into _runUpgradeClosurePull to skip unchanged INCREMENTAL doctypes, with graceful full-pull fallback on any failure. Note: sdk_log.dart and the sdkLog lines in sync_controller/sync_service are pre-existing refactor bits that ride along because these files are shared; the rest of that print->sdkLog refactor is intentionally left uncommitted.
Route stray print() diagnostics through sdkLog (a no-op in release builds) and drop the `// ignore: avoid_print` suppressions, importing sdk_log where needed. No behavioral change.
Resolve conflicts in auth.dart, mobile_home_screen.dart, frappe_sdk.dart, and sync_service.dart: - Adopt sdkLog over debugPrint/print per the offline-sync logging sweep. - Keep both screen_helpers and sdk_log imports in mobile_home_screen. - forTesting ctor: keep httpClient param + payloadTransformer init. - Closure pull: use the dhwani-ris#49 sync_details 'allowed' (skip-unchanged) set; fix List/Set mismatch since the refactor made _buildPullableDoctypes return a List (allowed = pullable.toSet(); allowed.difference(skip)). - sync_service: keep refactored _loadMetaFromDao, preserve the new pullOneInternalForTest helper, drop vestigial parse-catch tails. flutter analyze: clean. flutter test: 1333 passed.
# Conflicts: # .gitignore
…x types & terminal-error pause dhwani-ris#42 (A3): equal-jitter exponential backoff for GET retries — adds retryBackoffDelay() ([base/2, base], attempt-clamped, seedable) and wires both retry sites; de-synchronises simultaneous client reconnects. +5 tests. dhwani-ris#53 (A5): export OutboxRow/OutboxState/OutboxOperation/ErrorCode on the public barrel so consumers drop raw-SQL/implementation_imports access. Add ErrorCode.isTerminal (exhaustive, no default → new codes fail the build) and OutboxRow.isTerminal. New OutboxState.paused: a terminal server rejection (HTTP 417 validate-hook, 403, mandatory, link) is parked via OutboxDao.markPaused instead of markFailed — the push drain reads only `pending`, so it never loops; a corrected re-save collapses it back to pending (resume). UI (sync_error_banner, document_list badge) renders the paused state. +13 tests. dhwani-ris#52 (B1): integrated via merge of fix/label-overflow-ticker-crash (TickerProviderStateMixin + mapEquals guard + label Expanded wrap, +3 tests). Full suite: 1353 passing. flutter analyze clean (lib/). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The field displayed number-only but emitted +91-stored; form_builder's self patchValue echoed the stored value back into the same name-bound field, whose onChanged re-prefixed it (+9191...), never reaching a fixed point → stack overflow on the first keystroke (broke every form with a contact_number). Fix: decouple the visible controller (number-only, owned by a private stateful _PhoneNumberInput) from the FormBuilder value (+91 stored) via FormBuilderField, reconciling external patches with an echo guard; make toStored idempotent as defense-in-depth. Validation + +91 save contract + prefill preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FrappeSDK.dispose() closes the shared notifier, but an in-flight PushEngine/PullEngine/SyncController (all wired to the same instance by SyncEngineBuilder) can resume from an await and write notifier.value afterwards, hitting StreamController.add on a closed controller and throwing StateError during logout/shutdown. dispose() only drained _pendingDrain, not controller-driven syncs. Guard the setter with an isClosed check so a late write from any writer is a safe no-op. Update the notifier test to the new no-throw contract and add a regression test for the teardown race.
Move the per-fieldtype normalization switch and the multiselect helper out of the FrappeFormBuilder widget into a pure, headless FieldNormalizer so the rules can be unit-tested directly. Behavior unchanged; the widget delegates at both call sites. Adds 22 characterization tests covering every branch.
_handleLoginSuccess and build() re-instantiated MetaService, OfflineRepository, SyncService and a hand-rolled UnifiedResolver that diverged from the SDK's own wiring. Those blocks only ran when initialize() failed, but build() returns the error screen first, so they were unreachable. Remove them; rely on sdk.meta/repository/sync/ linkOptions with a "not initialized" guard.
…/sdk-offline-phase2
…ations Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hanged stream - Add injectDao/dao/loadFromCache/refreshAsync/setCurrentLangForTesting - setLocale is now cache-first (SQLite) + unconditional background refresh - Add onChanged broadcast stream that emits after cache is populated - FrappeClient field made nullable (Dart 3 sound null safety; production unaffected) - Update pre-existing setLocale test to reflect new fire-and-forget contract Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cking translation fetch
- Add import for TranslationDao in frappe_sdk.dart
- Both construction sites (_doInitialize and forTesting factory) now inject a TranslationDao instance via ..injectDao()
- Replace blocking await _translationService?.loadTranslations('en') with fire-and-forget refreshAsync() — removes 1 network round-trip from the boot critical path
- Add setLocale() call after restoreFromDb() to warm the SQLite translation cache from a persisted SessionUser language preference (currently inert; becomes effective when language persistence lands in Task 5)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…down Ensures TranslationDao and the onChanged StreamController are properly closed when the SDK disposes, preventing resource leaks. The dispose() method is called from FrappeSDK.dispose() during the teardown phase alongside other services. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…glish stored keys Splits _getOptions() into _getRawOptions() (English keys, used for all value/equality/onChanged paths) and _getOptions() (translated display labels, used only as Text children in DropdownMenuItem and FormBuilderFieldOption). Fixes all four correctness sites: item display, multi-select matching, single-select re-hydration, and single-option auto-select. Adds 5 tests covering translation rendering, no-translate passthrough, item value invariant, stored-value passthrough, and auto-select key emission. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sertion, trim options - Extend _field() helper with allowMultiple param (no copyWith needed on DocField) - Add 3 multi-select tests: display labels translated, tap stores English key, single-option auto-select emits English key - Replace vacuous Test 4 (isNull) with assertions that verify item values are English keys and item children show translated labels - Fix _getRawOptions() to trim-then-filter (consistent with _valueToList) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ry DAO in forTesting - Add isClosed guards to both _changedController.add(null) call sites in TranslationService (loadFromCache and _doRefresh) so a dispose() during an in-flight background refresh never throws a StateError. - Switch FrappeSDK.forTesting to TranslationDao.forTesting() (in-memory) so tests never touch the filesystem. - Add two dispose-safety tests covering no-StateError and no-emission- after-dispose scenarios.
…e hook, ARB priority, full UI coverage - lib/src/utils/translate.dart: FrappeTranslations static registry + tr() global helper exported from frappe_mobile_sdk.dart; bridges SDK widgets to host-app ARB lookup (renamed from __ — Dart treats leading-underscore identifiers as library-private) - TranslationService.translateDelegate: nullable hook for host app to intercept translation queries; ARB lookup wins over SQLite cache when result differs from source - TranslationService.translateLocal: renamed inner method for SQLite-only lookup - All SDK screens and field widgets migrated to tr(): form_screen, login_screen, document_list_screen, sync_status_screen, base_field, field_helpers, select_field, link_field, numeric_field, phone_field, searchable_select, table_multi_select, form_builder, sync_error_banner, sync_status_bar, app_guard, dialogs - doc/TRANSLATIONS.md: complete consumer guide covering architecture, ARB priority, SQLite fallback, locale lifecycle, reactive rebuild path, and testing patterns - Lint: remove redundant flutter/services.dart imports (data_field, text_field), add braces in form_screen if-statement, rename _service() test helper to makeService()
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Cumulative merge from the
ankitDhwanifork incorporating upstreamom/logic_correction(PR #74 by @om-dhwani-ris) plus additional offline-sync hardening, issue fixes, and the offline translations feature.Issues Closed
From upstream PR #74 (om/logic_correction)
PullApplymatches a pulled parent bymobile_uuidwhenserver_nameisNULLon pushINSERT; reconciles without false conflict when localserver_nameisNULLSyncMode {full, pushOnly, pullOnly}added toSyncController.syncNow/sync_detailspre-flight client (DoctypeService.getSyncDetails) +SyncStateNotifierwrite-after-close guardPullEnginecursor checkpointed (complete:false) after every applied batch so a mid-sync kill resumes cleanlyFrom local branches
SyncControllerretry + terminal-error pauseInputDecoration(duplicate label/helper-text) +PhoneFieldStackOverflowrecursion on text inputOutboxEntry,SyncState, and terminal-error types from the public surfaceAnimationControllerreused)New Feature: Offline Translations
Frappe backends already expose ~840 Hindi / ~1030 Gujarati keys via
mobile_auth.get_translations. This feature persists them to a dedicatedtranslations_cache.dbSQLite KV store so form field labels and select options render in the correct language from the first frame, even offline.What changed
lib/src/database/daos/translation_dao.dartbulkUpsert,readAll, in-memory variant for tests)lib/src/services/translation_service.dartloadFromCache(SQLite, <5 ms, no network),refreshAsync(fire-and-forget fetch+persist),onChangedstream,dispose()lib/src/sdk/frappe_sdk.dartrefreshAsyncreplaces blocking fetch;loadFromCachein restore-session path;dispose()propagatedlib/src/ui/widgets/fields/select_field.dart_getRawOptions()/_getOptions()split — translated labels displayed, English keys stored (matches Frappe web behaviour)Behaviour
Other Fixes (also in PR #74)
FieldNormalizerextracted fromFrappeFormBuilderinto a pure headless normalizerprint()replaced withsdkLogacross all services and UITest Plan
flutter test— 1408/1408 passingfull,pushOnly,pullOnly