Compare commits

..

34 Commits

Author SHA1 Message Date
Ron
233154ecb2 Merge pull request #492 from fleetbase/dev-v0.7.26
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.7.26
2026-01-16 16:18:02 +08:00
Ronald A. Richardson
04dd58397e Updated RELEASE notes 2026-01-16 16:11:08 +08:00
Ronald A. Richardson
6e537daf24 upgraded all dependencies 2026-01-16 15:50:23 +08:00
Ron
662c5ab716 Merge pull request #488 from fleetbase/feature/onboarding-orchestrator-enhancements
Feature/onboarding orchestrator enhancements
2026-01-16 15:30:57 +08:00
Ronald A. Richardson
502363efc4 Upgraded frankenphp image 2026-01-16 15:29:10 +08:00
roncodes
b7d8a58861 fix: Filter sensitive fields from onboarding context persistence
Added password filtering to onboarding-context service:
- merge() method now filters out password and password_confirmation
- set() method returns early for sensitive fields
- Prevents passwords from being stored in localStorage/appCache

This ensures sensitive user data is never persisted in the browser.
2026-01-08 03:33:39 -05:00
roncodes
11f6d173b7 feat: Add navigation history persistence to orchestrator
- Persist navigation history to localStorage on each step
- Restore history when resuming from previous session
- Clear history when flow completes
- Fixes back button functionality after page refresh
- Uses flow-specific storage key pattern: onboarding:history:{flowId}
2026-01-07 03:33:35 -05:00
Ronald A. Richardson
7d232597ff feat: working on onboarding orchestrator enhancements 2026-01-07 16:31:52 +08:00
Ron
b25da51496 Merge pull request #486 from fleetbase/dev-v0.7.25
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
feat: New SMS service to support multiple SMS providers + framework improvements
2025-12-29 21:02:03 +08:00
Ronald A. Richardson
7d776f2bd5 updated pnpm lockfile overrides 2025-12-29 20:48:29 +08:00
Ronald A. Richardson
ecfcec72e4 updated override versions 2025-12-29 20:39:25 +08:00
Ronald A. Richardson
ca1741a4b2 feat: New SMS service to support multiple SMS providers + framework improvements 2025-12-29 20:36:43 +08:00
Ron
947565bcf0 Merge pull request #483 from fleetbase/dev-v0.7.24
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
Fix: Critical cache key collision bug in ApiModelCache
2025-12-21 12:05:16 +08:00
Ronald A. Richardson
2d4cc5cf66 Fix: Critical cache key collision bug in ApiModelCache 2025-12-21 12:02:53 +08:00
Ronald A. Richardson
53a87d6f38 Hotfix: load iam engine for user-form modal when creating driver
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
2025-12-19 23:17:51 +08:00
Ronald A. Richardson
d7f8f87315 Hotfix: onboarding wrapper stylings added back 2025-12-19 22:58:29 +08:00
Ron
36673ef564 Merge pull request #482 from fleetbase/dev-v0.7.23
dev-v0.7.23
2025-12-19 22:41:13 +08:00
Ronald A. Richardson
19341c81e7 Minor tweaks to user model and profile page 2025-12-19 22:39:41 +08:00
Ronald A. Richardson
b4ecf5bda9 bump FLEETBASE_VERSION in Dockerfile 2025-12-19 22:24:38 +08:00
Ronald A. Richardson
1b714a7ef8 Release ready 2025-12-19 22:23:39 +08:00
Ron
e41cd62ea5 Merge pull request #481 from fleetbase/feature/onboarding-wrapper-architecture
feat: Add wrapper component support to onboarding orchestrator
2025-12-19 15:56:49 +08:00
Ronald A. Richardson
1ca1342052 feat: fixed optimization changes for octane, added deprecation workflow 2025-12-19 15:56:03 +08:00
roncodes
a5a5ddb0d5 perf: Optimize FrankenPHP/Octane configuration for high load
**Changes:**

1. **Caddyfile**:
   - Reduced num_threads from 24 to 20
   - Added request timeouts (read_body: 10s, write: 60s, idle: 120s)
   - With 4 containers: 20 × 4 = 80 total workers

2. **Dockerfile**:
   - Added explicit --workers=20 to octane:frankenphp command
   - Increased --max-requests from 250 to 1000
   - Applied to app-dev, app-release, and app stages

3. **Octane config**:
   - Enabled DisconnectFromDatabases listener
   - Enabled CollectGarbage listener
   - Prevents DB connection leaks and memory leaks

**Impact:**
- Better resource management under load
- Prevents connection pool exhaustion
- Requires db.t3.large (591 max connections) or better
- Supports up to 250 concurrent VUs

**Related:**
- Requires RDS upgrade from db.t4g.micro to db.t3.large
- Works with DB_CONNECTION_POOL_SIZE=25 (100 total connections)
- See configuration-analysis.md for details
2025-12-16 20:06:35 -05:00
Ronald A. Richardson
c51f3ca6c8 v0.7.23 2025-12-17 08:57:41 +08:00
roncodes
a9b172081a feat: Add lifecycle hooks support to onboarding orchestrator
- Add onFlowWillStart, onFlowDidStart, onStepWillChange, onStepDidChange, onFlowWillEnd, onFlowDidEnd hooks
- Hooks are optional and backward compatible with existing flows
- Add getCurrentPath() and isStepInPath() helper methods for multi-path flows
- Support dynamic next() functions (already existed, now documented)
- Maintain full backward compatibility with default@v1 flow
2025-12-11 23:10:23 -05:00
Ronald A. Richardson
a29ca0ecb9 feat: update onboarding-registry service to allow set default onboard flow on registration 2025-12-09 09:48:23 +08:00
roncodes
6442644438 feat: Add wrapper component support to onboarding orchestrator
- Add wrapper property to OnboardingOrchestratorService
- Update onboarding/yield component to render wrapper using lazy-engine-component
- Clean up onboard.hbs template to remove styling constraints
- Enable extensions to provide custom wrapper components for onboarding flows
2025-12-08 20:38:59 -05:00
Ron
0238632fdd Merge pull request #477 from fleetbase/dev-v0.7.22
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
Organizations can now set their own alpha-numeric sender ID for SMS
2025-12-07 06:37:02 +00:00
Ronald A. Richardson
27652db9c3 Organizations can now set their own alpha-numeric sender ID for SMS 2025-12-07 14:15:31 +08:00
Ronald A. Richardson
5eaf2039d4 Fix: correct version of ember-ui in console/package.json
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
2025-12-06 21:49:14 +08:00
Ron
d8c06ae0be Merge pull request #476 from fleetbase/dev-v0.7.21
v0.7.21 ~ 5x faster css compiling and flawless builds
2025-12-06 13:41:42 +00:00
Ronald A. Richardson
c9011b3ffa *critical* patch to load-extensions initializer 2025-12-06 21:38:02 +08:00
Ronald A. Richardson
520de0f6bc fixed formating on console/package.json 2025-12-06 19:39:24 +08:00
Ronald A. Richardson
ba6ed235e3 v0.7.21 ~ 5x faster css compiling and flawless builds 2025-12-06 19:26:17 +08:00
55 changed files with 1854 additions and 1712 deletions

View File

@@ -13,9 +13,24 @@ env:
GITHUB_AUTH_KEY: ${{ secrets._GITHUB_AUTH_TOKEN }}
jobs:
setup:
name: Setup Environment
runs-on: ubuntu-latest
outputs:
environment: ${{ steps.extract-env.outputs.environment }}
steps:
- name: Extract environment from branch name
id: extract-env
run: |
ENVIRONMENT=$(echo "${{ github.ref_name }}" | sed 's|deploy/||')
echo "environment=${ENVIRONMENT}" >> $GITHUB_OUTPUT
echo "Deploying to environment: ${ENVIRONMENT}"
build_service:
name: Build and Deploy the Service
needs: [setup]
runs-on: ubuntu-latest
environment: ${{ needs.setup.outputs.environment }}
permissions:
id-token: write # This is required for requesting the JWT
contents: read # This is required for actions/checkout
@@ -120,8 +135,9 @@ jobs:
build_frontend:
name: Build and Deploy the Console
needs: [build_service]
needs: [setup, build_service]
runs-on: ubuntu-latest
environment: ${{ needs.setup.outputs.environment }}
permissions:
id-token: write # This is required for requesting the JWT
contents: read # This is required for actions/checkout
@@ -192,20 +208,17 @@ jobs:
fi
working-directory: ./console
- name: Set Env Variables for QA
if: startsWith(github.ref, 'refs/heads/deploy/qa')
- name: Set Env Variables
run: |
echo "STRIPE_KEY=${{ secrets.STRIPE_TEST_KEY }}" >> ./environments/.env.production
echo "EXTENSIONS=${{ secrets.EXTENSIONS }}" >> ./environments/.env.production
echo "LOGROCKET_APP_ID=${{ secrets.LOGROCKET_APP_ID }}" >> ./environments/.env.production
echo "EXTENSIONS=@fleetbase/billing-engine,@fleetbase/internals-engine" >> ./environments/.env.production
working-directory: ./console
- name: Set Env Variables for Production
if: startsWith(github.ref, 'refs/heads/deploy/production')
run: |
echo "STRIPE_KEY=${{ secrets.STRIPE_KEY }}" >> ./environments/.env.production
echo "LOGROCKET_APP_ID=${{ secrets.LOGROCKET_APP_ID }}" >> ./environments/.env.production
echo "EXTENSIONS=@fleetbase/billing-engine,@fleetbase/internals-engine" >> ./environments/.env.production
echo "STRIPE_PRICE_LICENSE_ANNUAL_ID=${{ secrets.STRIPE_PRICE_LICENSE_ANNUAL_ID }}" >> ./environments/.env.production
echo "STRIPE_PRICE_LICENSE_MONTHLY_ID=${{ secrets.STRIPE_PRICE_LICENSE_MONTHLY_ID }}" >> ./environments/.env.production
echo "STRIPE_PRICE_LICENSE_MAJOR_ID=${{ secrets.STRIPE_PRICE_LICENSE_MAJOR_ID }}" >> ./environments/.env.production
echo "STRIPE_PRICE_LICENSE_MINOR_ID=${{ secrets.STRIPE_PRICE_LICENSE_MINOR_ID }}" >> ./environments/.env.production
echo "STRIPE_PRICE_INSTALLATION_FULL_ID=${{ secrets.STRIPE_PRICE_INSTALLATION_FULL_ID }}" >> ./environments/.env.production
echo "STRIPE_PRICE_INSTALLATION_ASSISTED_ID=${{ secrets.STRIPE_PRICE_INSTALLATION_ASSISTED_ID }}" >> ./environments/.env.production
working-directory: ./console
- name: Install dependencies

View File

@@ -8,6 +8,7 @@
http://:8000 {
root * /fleetbase/api/public
encode zstd br gzip
php_server {
resolve_root_symlink
}

View File

@@ -1,281 +1,24 @@
# 🚀 Fleetbase v0.7.20 — 2025-12-05
# 🚀 Fleetbase v0.7.26 — 2025-01-16
> "Major architectural & universe framework refactor for improved build, loading and initialization of extensions and system."
> "Improved Driver Validation + API improvements"
---
## @fleetbase/ember-core v0.3.7
### 🎯 Major Features
#### Universe Service Refactor
Complete architectural overhaul of the Universe service with separation of concerns into specialized sub-services:
- **ExtensionManager**: Manages extension loading, engine instances, and lifecycle hooks
- **RegistryService**: Fully dynamic, Map-based registry system for cross-engine resource sharing
- **MenuService**: Dedicated menu item and panel management with event support
- **WidgetService**: Dashboard widget registration and retrieval system
- **HookService**: Application lifecycle hooks and custom event hooks
**Key Benefits:**
- Improved performance with lazy loading and proper engine lifecycle management
- Better separation of concerns and maintainability
- Enhanced cross-engine resource sharing via Ember's application container
- Backward compatibility maintained through facade pattern
#### Enhanced Extension System
**New Extension Patterns:**
- `extension.js` hook patterns with `setupExtension()` and `onEngineLoaded()` support
- `whenEngineLoaded()` utility method that handles both loaded and not-yet-loaded engines
- Comprehensive extension checking methods: `isInstalled()`, `isEngineLoaded()`, `isEngineLoading()`
- Automatic hook execution for router-loaded engines
**Engine Loading Improvements:**
- Fixed race conditions between engine loading and hook registration
- Proper tracking of router-loaded engines in `loadedEngines` Map
- Patched `buildChildEngineInstance` to run hooks for router-loaded engines
- Two-phase extension setup: registration first, then hook execution
**Performance Optimizations:**
- `extensions.json` loading with localStorage caching
- Reduced console noise by removing excessive debug logs
- Performance monitoring for extension load times
#### Helper Registration System
- Cross-engine helper sharing via application container
- `registerHelper()` method for template helpers
- Support for both class-based and function-based helpers
- Automatic registration to application container for global access
#### Component Registration Enhancements
- `registerRenderableComponent()` for cross-engine component sharing
- Support for component classes via `ExtensionComponent`
- Automatic component registration to engine containers
- `getRenderableComponents()` for retrieving registered components
### 🔧 API Improvements
#### UniverseService
- `getService()` method with flexible naming pattern resolution:
- Short names: `"menu"``universe/menu-service`
- Plural variations: `"hooks"` or `"hook"``universe/hook-service`
- CamelCase: `"menuService"``universe/menu-service`
- Full names: `"menu-service"``universe/menu-service`
- With namespace: `"universe/menu-service"``universe/menu-service`
- `whenEngineLoaded()` for simplified engine-dependent setup
- `getServiceFromEngine()` for accessing engine-specific services
- `getApplicationInstance()` with fallback to `window.Fleetbase`
#### MenuService
- Computed getters for template access: `headerMenuItems`, `adminMenuItems`, `adminMenuPanels`
- Event triggers for backward compatibility
- Proper `finalView` normalization for all menu items
- `onClick` handlers wrapped with `menuItem` and `universe` parameters
- Fixed panel item slug/view structure to match original behavior
#### WidgetService
- Per-dashboard widget registration
- `registerWidgets()` for making widgets available on dashboards
- `registerDefaultWidgets()` for auto-loading specific widgets
- Proper widget filtering by dashboard ID
### 🐛 Bug Fixes
#### Extension Loading
- Resolved timeout errors during engine boot
- Fixed duplicate dashboard ID errors
- Corrected engine instance tracking and retrieval
- Fixed race conditions in extension setup
#### Menu System
- Fixed registry name mismatch for header menu items
- Corrected panel item structure for proper routing
- Added null checks before `dasherize` calls
- Fixed `onClick` handler parameter passing
#### Registry System
- Fixed widget registry lookup to work with array storage
- Proper filtering by registration key
- Shared registries Map across all engines via application container
- Triggered reactivity when creating new registries
#### Service Resolution
- Fixed service injection on engine boot
- Corrected service map injection
- Added missing `getServiceFromEngine` method
- Fixed `virtualRouteRedirect` and `getViewFromTransition` methods
### 📦 Exports & Structure
- Named exports on "exports" namespace
- Separated default and named exports in app/exports re-exports
### 🔄 ResourceActionService
- Allow properties to be set from initialization options
- Improved flexibility for service configuration
## @fleetbase/ember-ui v0.3.12
### 🎯 Major Features
#### Dashboard Service Refactor
Complete rewrite to support the new Universe service architecture:
- Direct injection of `widgetService` instead of going through universe
- Updated to use new widget service API
- Proper dashboard cleanup and recreation on route revisit
- Race condition fixes with `drop` task modifier
#### Widget Panel Enhancements
**Search & Filter:**
- Real-time keyword search for widgets by name and description
- `@tracked searchQuery` with `updateSearchQuery` action
- Improved widget discoverability
**Floating Pagination:**
- New floating pagination style (bottom-right, rounded, shadowed)
- Reveals horizontal scrollbar for better table navigation
- `@useTfootPagination` arg for backward compatibility
- Default to floating, opt-in to table footer pagination
**Thin Scrollbars:**
- Consistent 8px scrollbar height for both axes
- Webkit scrollbar styles for modern browsers
- Firefox `scrollbar-width: thin` support
- Dark mode scrollbar colors
### 🔧 Component Improvements
#### LazyEngineComponent
- Support for both path-based (lazy) and class-based (immediate) components
- Automatic component registration to engine containers
- Proper engine component lookup
- Helper for use with `{{component}}` syntax
#### RegistryYield
- Converted `yieldables` to computed getter for automatic reactivity
- Automatic wrapping of components with `LazyEngineComponent`
- `isComponent` getter to detect component types vs menu items
- Updated to use `registryService.getRenderableComponents()`
- Removed event listener approach (now uses reactive getters)
#### LoadEngine Component
- New component for explicit engine loading
- Complements `LazyEngineComponent` for different use cases
### 🐛 Bug Fixes
#### Dashboard Service
- Fixed duplicate dashboard ID errors with `drop` task modifier
- Added try-catch in `_createDefaultDashboard` for race conditions
- Proper state checks (`isDeleted`, `isDestroying`, `isDestroyed`)
- Fixed reset() to unload dashboard-widgets before dashboards
- Prevents identity map conflicts when recreating dashboards
#### Widget Panel
- Fixed `availableWidgets` reactivity by converting to getter
- Use `args.defaultDashboardId` first, then fallback to `this.defaultDashboardId`
- Ensures widgets registered after component creation are visible
- Handles string-represented widgets and components
#### RegistryYield
- Fixed TypeError by removing `registryService.on` event listener
- RegistryService doesn't have Evented mixin, now uses reactive getters
- Components registered after construction now properly yielded
#### Table Styling
- Fixed floating pagination spacing (reduced padding, adjusted margins)
- Applied `overflow-x` to `.next-table-wrapper` instead of table
- Horizontal scrollbar now visible and not hidden by pagination
- Added `overflow-y: visible` to prevent vertical scroll issues
### 🎨 UX Improvements
- Minor UX tweaks to widget panel component
- Improved widget panel layout and spacing
- Better visual hierarchy for widget selection
- Enhanced table navigation with floating pagination
## Migration Guide
### For Extension Developers
#### Using the New Extension Patterns
**Before:**
```javascript
// In addon/engine.js
export default class MyEngine extends Engine {
setupExtension() {
// Setup code here
}
}
```
**After:**
```javascript
// In addon/extension.js
import { Widget } from '@fleetbase/ember-core/contracts';
export default {
setupExtension(universe) {
// Get widget service
const widgetService = universe.getService('widget');
// Register widgets, menu items, etc.
widgetService.registerWidgets('dashboard', [new Widget({ ... })]);
// Register hooks for when other engines load
universe.whenEngineLoaded('@fleetbase/fleetops-engine', (engineInstance) => {
// Setup integration with FleetOps
});
}
}
```
#### Accessing Universe Sub-Services
**Flexible Service Resolution:**
```javascript
// All of these work:
const menuService = universe.getService('menu');
const menuService = universe.getService('menu-service');
const menuService = universe.getService('menuService');
const menuService = universe.getService('universe/menu-service');
const hookService = universe.getService('hooks');
const widgetService = universe.getService('widgets');
const registry = universe.getService('registry');
```
#### Widget Registration
**Before:**
```javascript
universe.registerWidget(widgetObject);
```
**After:**
```javascript
// Register widgets available for a dashboard
universe.registerWidgets('dashboard', [widget1, widget2]);
// Register widgets that auto-load on a dashboard
universe.registerDefaultWidgets('dashboard', [widget1]);
```
## ✨ Highlights
- Improved driver creation validation for internal API
- Vehicle and Driver API use explicit `::create` method now
- Improved onboarding orchestrator framework and services for history and resume capability
- Upgraded Stripe SDK to v17
---
## ⚠️ Breaking Changes
- **NO MAJOR BREAKING CHANGED** Both releases maintain full backward compatibility with existing code. The refactor uses a facade pattern to ensure all existing APIs continue to work while providing new, improved patterns for future development.
### Notice
- Extensions must adapt to the new `extension.js` pattern for initialization setup.
- Extension dependencies are not auto-loaded and initialized, dependencies must be explicitly loaded from the setup `extension.js`
- `UniverseService` has major changes which refactored core responsibilities to sub-services. There is backwards compatability, but plan to migrate to the new integration architecture.
- None 🙂
---
## 🔧 Upgrade Steps
```bash
# Pull latest version
git pull origin main --no-rebase
@@ -288,12 +31,7 @@ docker compose down && docker compose up -d
docker compose exec application bash -c "./deploy.sh"
```
### Post-Upgrade
1. Clear browser cache and localStorage
2. Restart your development server
3. Test extension loading and dashboard functionality
4. Check console for any deprecation warnings
---
## Need help?
Join the discussion on [GitHub Discussions](https://github.com/fleetbase/fleetbase/discussions) or drop by [#fleetbase on Discord](https://discord.com/invite/HnTqQ6zAVn)
Join the discussion on [GitHub Discussions](https://github.com/fleetbase/ember-ui/discussions) or drop by [#fleetbase on Discord](https://discord.com/invite/HnTqQ6zAVn)

View File

@@ -40,7 +40,6 @@ class Kernel extends HttpKernel
],
'api' => [
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];

View File

@@ -18,12 +18,12 @@
}
],
"require": {
"php": ">=8.0 <=8.2.28",
"php": ">=8.0 <=8.2.30",
"appstract/laravel-opcache": "^4.0",
"fleetbase/core-api": "^1.6.27",
"fleetbase/fleetops-api": "^0.6.28",
"fleetbase/registry-bridge": "^0.1.1",
"fleetbase/storefront-api": "^0.4.8",
"fleetbase/core-api": "^1.6.34",
"fleetbase/fleetops-api": "^0.6.33",
"fleetbase/registry-bridge": "^0.1.3",
"fleetbase/storefront-api": "^0.4.12",
"guzzlehttp/guzzle": "^7.0.1",
"laravel/framework": "^10.0",
"laravel/octane": "^2.3",
@@ -36,7 +36,7 @@
"psr/http-factory-implementation": "*",
"resend/resend-php": "^0.14.0",
"s-ichikawa/laravel-sendgrid-driver": "^4.0",
"stripe/stripe-php": "13.13.0",
"stripe/stripe-php": "^17.0",
"symfony/mailgun-mailer": "^7.1",
"symfony/postmark-mailer": "^7.1"
},

884
api/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@ return [
'allowed_headers' => ['*'],
'exposed_headers' => ['x-compressed-json', 'access-console-sandbox', 'access-console-sandbox-key'],
'exposed_headers' => ['x-compressed-json', 'access-console-sandbox', 'access-console-sandbox-key', 'content-disposition'],
'max_age' => 0,

View File

@@ -51,7 +51,7 @@ return [
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['single'],
'channels' => ['single', 'stdout'],
'ignore_exceptions' => false,
],

View File

@@ -105,8 +105,8 @@ return [
OperationTerminated::class => [
FlushOnce::class,
FlushTemporaryContainerInstances::class,
// DisconnectFromDatabases::class,
// CollectGarbage::class,
DisconnectFromDatabases::class,
CollectGarbage::class,
],
WorkerErrorOccurred::class => [

View File

@@ -43,4 +43,10 @@ return [
'secret' => env('STRIPE_SECRET', env('STRIPE_API_SECRET')),
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
],
'callpromn' => [
'api_key' => env('CALLPROMN_API_KEY', ''),
'from' => env('CALLPROMN_FROM', ''),
'base_url' => env('CALLPROMN_BASE_URL', 'https://api.messagepro.mn' ),
],
];

View File

@@ -2,6 +2,7 @@ import Application from '@ember/application';
import Resolver from 'ember-resolver';
import loadInitializers from 'ember-load-initializers';
import config from '@fleetbase/console/config/environment';
import './deprecation-workflow';
export default class App extends Application {
modulePrefix = config.modulePrefix;

View File

@@ -1,31 +0,0 @@
<Overlay @isOpen={{@isOpen}} @onLoad={{this.setOverlayContext}} @position="right" @noBackdrop={{true}} @fullHeight={{true}} @width={{or this.width @width "400px"}}>
<Overlay::Header @title={{t "component.dashboard-widget-panel.select-widgets"}} @hideStatusDot={{true}} @titleWrapperClass="leading-5">
<div class="flex flex-1 justify-end">
<Button @type="default" @icon="times" @helpText={{t "component.dashboard-widget-panel.close-and-save"}} @onClick={{this.onPressClose}} />
</div>
</Overlay::Header>
<Overlay::Body @wrapperClass="new-service-rate-overlay-body px-4 space-y-4 pt-4">
<div class="grid grid-cols-1 gap-4 text-xs dark:text-gray-100">
{{#each this.availableWidgets as |widget|}}
<div
class="rounded-lg border border-gray-300 bg-white dark:border-gray-700 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-300 ease-in-out shadow-md px-4 py-2 cursor-pointer"
{{on "click" (fn this.addWidgetToDashboard widget)}}
>
<div class="flex flex-row items-center leading-6 mb-2.5">
<div class="w-8 flex items-center justify-start">
<FaIcon @icon={{widget.icon}} class="text-lg text-gray-600 dark:text-gray-300" />
</div>
<p class="text-sm truncate font-semibold dark:text-gray-100 text-gray-800">
{{t "component.dashboard-widget-panel.widget-name" widgetName=widget.name}}
</p>
</div>
<div>
<p class="text-xs dark:text-gray-100 text-gray-800">{{widget.description}}</p>
</div>
</div>
{{/each}}
</div>
<Spacer @height="300px" />
</Overlay::Body>
</Overlay>

View File

@@ -1,60 +0,0 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
export default class DashboardWidgetPanelComponent extends Component {
@service universe;
@tracked availableWidgets = [];
@tracked dashboard;
@tracked isOpen = true;
@service notifications;
/**
* Constructs the component and applies initial state.
*/
constructor(owner, { dashboard }) {
super(...arguments);
this.availableWidgets = this.universe.getDashboardWidgets();
this.dashboard = dashboard;
}
/**
* Sets the overlay context.
*
* @action
* @param {OverlayContextObject} overlayContext
*/
@action setOverlayContext(overlayContext) {
this.context = overlayContext;
if (typeof this.args.onLoad === 'function') {
this.args.onLoad(...arguments);
}
}
@action addWidgetToDashboard(widget) {
// If widget is a component definition/class
if (typeof widget.component === 'function') {
widget.component = widget.component.name;
}
this.args.dashboard.addWidget(widget).catch((error) => {
this.notifications.serverError(error);
});
}
/**
* Handles cancel button press.
*
* @action
*/
@action onPressClose() {
this.isOpen = false;
if (typeof this.args.onClose === 'function') {
this.args.onClose();
}
}
}

View File

@@ -1,42 +1,61 @@
<div class="bg-white dark:bg-gray-800 py-5 px-4 shadow rounded-lg w-full">
<div class="mb-4">
<Image src={{@brand.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} height="56" class="h-10 object-contain mx-auto" />
<div class="mt-2">
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{t "onboard.index.title"}}
</h2>
<div class="flex items-center justify-center h-screen min-h-screen px-4 py-12 bg-gray-50 dark:bg-gray-900 sm:px-6 lg:px-8 overflow-y-scroll">
<div class="w-full max-w-md h-screen flex items-center justify-center py-4">
<div class="bg-white dark:bg-gray-800 py-5 px-4 shadow rounded-lg w-full">
<div class="mb-4">
<Image src={{@brand.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} height="56" class="h-10 object-contain mx-auto" />
<div class="mt-2">
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{t "onboard.index.title"}}
</h2>
</div>
</div>
<div class="flex px-3 py-2 mb-4 rounded-md shadow-sm bg-blue-200">
<div>
<FaIcon @icon="hand-spock" @size="lg" class="text-blue-900 mr-4" />
</div>
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
{{t "onboard.index.welcome-title" htmlSafe=true companyName=(t "app.name")}}
{{t "onboard.index.welcome-text"}}
</p>
</div>
<form {{on "submit" (perform this.onboard)}}>
{{#if this.error}}
<InfoBlock @icon="exclamation-triangle" @text={{this.error}} class="mb-6 px-3 py-2 bg-red-300 text-red-900" @textClass="text-red-900" />
{{/if}}
<InputGroup @name={{t "onboard.index.full-name"}} @value={{this.name}} @helpText={{t "onboard.index.full-name-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.your-email"}} @type="email" @value={{this.email}} @helpText={{t "onboard.index.your-email-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.phone"}} @helpText={{t "onboard.index.phone-help-text"}}>
<PhoneInput @onInput={{fn (mut this.phone)}} class="form-input input-lg w-full" />
</InputGroup>
<InputGroup @name={{t "onboard.index.organization-name"}} @value={{this.organization_name}} @helpText={{t "onboard.index.organization-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.password"}} @value={{this.password}} @type="password" @helpText={{t "onboard.index.password-help-text"}} @inputClass="input-lg" />
<InputGroup
@name={{t "onboard.index.confirm-password"}}
@value={{this.password_confirmation}}
@type="password"
@helpText={{t "onboard.index.confirm-password-help-text"}}
@inputClass="input-lg"
/>
<div class="flex items-center justify-end mt-5">
<Button
@buttonType="submit"
@icon="check"
@iconPrefix="fas"
@type="primary"
@size="lg"
@text={{t "onboard.index.continue-button-text"}}
@isLoading={{this.onboard.isRunning}}
@disabled={{not this.filled}}
/>
</div>
</form>
<RegistryYield @registry="onboard" as |YieldedComponent ctx|>
<YieldedComponent @context={{ctx}} />
</RegistryYield>
</div>
</div>
<div class="flex px-3 py-2 mb-4 rounded-md shadow-sm bg-blue-200">
<div>
<FaIcon @icon="hand-spock" @size="lg" class="text-blue-900 mr-4" />
</div>
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
{{t "onboard.index.welcome-title" htmlSafe=true companyName=(t "app.name")}}
{{t "onboard.index.welcome-text"}}
</p>
</div>
<form {{on "submit" (perform this.onboard)}}>
{{#if this.error}}
<InfoBlock @icon="exclamation-triangle" @text={{this.error}} class="mb-6 px-3 py-2 bg-red-300 text-red-900" @textClass="text-red-900" />
{{/if}}
<InputGroup @name={{t "onboard.index.full-name"}} @value={{this.name}} @helpText={{t "onboard.index.full-name-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.your-email"}} @type="email" @value={{this.email}} @helpText={{t "onboard.index.your-email-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.phone"}} @helpText={{t "onboard.index.phone-help-text"}}>
<PhoneInput @onInput={{fn (mut this.phone)}} class="form-input input-lg w-full" />
</InputGroup>
<InputGroup @name={{t "onboard.index.organization-name"}} @value={{this.organization_name}} @helpText={{t "onboard.index.organization-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.password"}} @value={{this.password}} @type="password" @helpText={{t "onboard.index.password-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.confirm-password"}} @value={{this.password_confirmation}} @type="password" @helpText={{t "onboard.index.confirm-password-help-text"}} @inputClass="input-lg" />
<div class="flex items-center justify-end mt-5">
<Button @buttonType="submit" @icon="check" @iconPrefix="fas" @type="primary" @size="lg" @text={{t "onboard.index.continue-button-text"}} @isLoading={{this.onboard.isRunning}} @disabled={{not this.filled}} />
</div>
</form>
<RegistryYield @registry="onboard" as |YieldedComponent ctx|>
<YieldedComponent @context={{ctx}} />
</RegistryYield>
</div>

View File

@@ -1,78 +1,82 @@
{{page-title (t "onboard.verify-email.header-title")}}
{{#if this.initialized}}
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg w-full">
<div class="mb-6">
<LinkTo @route="console" class="flex items-center justify-center">
<LogoIcon @size="12" class="rounded-md" />
</LinkTo>
<h2 class="mt-6 text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{t "onboard.verify-email.title"}}
</h2>
</div>
<div class="flex items-center justify-center h-screen min-h-screen px-4 py-12 bg-gray-50 dark:bg-gray-900 sm:px-6 lg:px-8 overflow-y-scroll">
<div class="w-full max-w-md h-screen flex items-center justify-center py-4">
{{#if this.initialized}}
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg w-full">
<div class="mb-6">
<LinkTo @route="console" class="flex items-center justify-center">
<LogoIcon @size="12" class="rounded-md" />
</LinkTo>
<h2 class="mt-6 text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{t "onboard.verify-email.title"}}
</h2>
</div>
<InfoBlock @type="info" @icon="shield-halved" @iconSize="lg">
{{t "onboard.verify-email.message-text" htmlSafe=true}}
</InfoBlock>
<InfoBlock @type="info" @icon="shield-halved" @iconSize="lg">
{{t "onboard.verify-email.message-text" htmlSafe=true}}
</InfoBlock>
<form class="mt-8 space-y-6" {{on "submit" (perform this.verify)}}>
<InputGroup
@type="tel"
@name={{t "onboard.verify-email.verification-input-label"}}
@value={{this.code}}
@helpText={{t "onboard.verify-email.verification-code-text"}}
@inputClass="input-lg"
{{on "input" this.verification.validateInput}}
{{did-insert this.verification.validateInput}}
/>
<form class="mt-8 space-y-6" {{on "submit" (perform this.verify)}}>
<InputGroup
@type="tel"
@name={{t "onboard.verify-email.verification-input-label"}}
@value={{this.code}}
@helpText={{t "onboard.verify-email.verification-code-text"}}
@inputClass="input-lg"
{{on "input" this.verification.validateInput}}
{{did-insert this.verification.validateInput}}
/>
<div class="flex flex-row items-center space-x-4">
<Button
@icon="check"
@iconPrefix="fas"
@buttonType="submit"
@type="primary"
@size="lg"
@text="Verify & Continue"
@isLoading={{this.verify.isRunning}}
@disabled={{not this.verification.ready}}
/>
<a href="#" {{on "click" this.verification.didntReceiveCode}} class="text-sm text-blue-400 hover:text-blue-300">{{t "onboard.verify-email.didnt-receive-a-code"}}</a>
</div>
<div class="flex flex-row items-center space-x-4">
<Button
@icon="check"
@iconPrefix="fas"
@buttonType="submit"
@type="primary"
@size="lg"
@text="Verify & Continue"
@isLoading={{this.verify.isRunning}}
@disabled={{not this.verification.ready}}
/>
<a href="#" {{on "click" this.verification.didntReceiveCode}} class="text-sm text-blue-400 hover:text-blue-300">{{t "onboard.verify-email.didnt-receive-a-code"}}</a>
</div>
{{#if this.verification.waiting}}
<div class="flex flex-col flex-grow-0 flex-shrink-0 text-sm bg-yellow-800 border border-yellow-600 px-2 py-2 rounded-md text-yellow-100 my-4 transition-all">
<div class="flex flex-row items-start mb-2">
<div class="w-8 flex-grow-0 flex-shrink-0">
<FaIcon @icon="triangle-exclamation" @size="xl" class="pt-1" />
</div>
<div class="flex-1">
<div class="flex-1 text-sm text-yellow-100">
<div>{{t "auth.verification.didnt-receive-a-code" htmlSafe=true}}</div>
<div>{{t "auth.verification.not-sent.alternative-choice" htmlSafe=true}}</div>
{{#if this.verification.waiting}}
<div class="flex flex-col flex-grow-0 flex-shrink-0 text-sm bg-yellow-800 border border-yellow-600 px-2 py-2 rounded-md text-yellow-100 my-4 transition-all">
<div class="flex flex-row items-start mb-2">
<div class="w-8 flex-grow-0 flex-shrink-0">
<FaIcon @icon="triangle-exclamation" @size="xl" class="pt-1" />
</div>
<div class="flex-1">
<div class="flex-1 text-sm text-yellow-100">
<div>{{t "auth.verification.didnt-receive-a-code" htmlSafe=true}}</div>
<div>{{t "auth.verification.not-sent.alternative-choice" htmlSafe=true}}</div>
</div>
</div>
</div>
<div class="flex items-center space-x-2">
<Button
@text={{t "auth.verification.not-sent.resend-email"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.verification.resendEmail}}
/>
<Button
@text={{t "auth.verification.not-sent.send-by-sms"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.verification.resendBySms}}
/>
</div>
</div>
</div>
<div class="flex items-center space-x-2">
<Button
@text={{t "auth.verification.not-sent.resend-email"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.verification.resendEmail}}
/>
<Button
@text={{t "auth.verification.not-sent.send-by-sms"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.verification.resendBySms}}
/>
</div>
</div>
{{/if}}
</form>
{{/if}}
</form>
</div>
{{/if}}
</div>
{{/if}}
</div>

View File

@@ -1,7 +1,9 @@
<section class="onboarding step-host">
{{#if this.initialized}}
{{#if this.currentComponent}}
{{component this.currentComponent context=this.context orchestrator=this.orchestrator brand=@brand}}
{{#if this.orchestrator.wrapper}}
{{component (lazy-engine-component this.orchestrator.wrapper) currentStepComponent=this.currentComponent context=this.context orchestrator=this.orchestrator brand=@brand}}
{{else if this.currentComponent}}
{{component (lazy-engine-component this.currentComponent) context=this.context orchestrator=this.orchestrator brand=@brand}}
{{/if}}
{{else}}
<div class="flex items-center justify-center min-h-24">

View File

@@ -68,6 +68,7 @@ export default class ConsoleAccountIndexController extends Controller {
subject_uuid: this.user.id,
subject_type: 'user',
type: 'user_avatar',
resize: 'md'
},
(uploadedFile) => {
this.user.setProperties({

View File

@@ -77,7 +77,7 @@ export default class ConsoleAdminOrganizationsIndexUsersController extends Contr
valuePath: 'roleName',
},
{
label: this.intl.t('common.phone-number'),
label: this.intl.t('common.phone'),
valuePath: 'phone',
},
{

View File

@@ -2,6 +2,9 @@ import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
export default class ConsoleAdminVirtualController extends Controller {
@tracked bodyClass = 'overflow-y-scroll h-full';
@tracked containerClass = 'container mx-auto h-screen';
@tracked wrapperClass = 'max-w-3xl my-10 mx-auto';
@tracked view;
queryParams = ['view'];
}

View File

@@ -6,35 +6,13 @@ import createNotificationKey from '@fleetbase/ember-core/utils/create-notificati
import { task } from 'ember-concurrency';
export default class ConsoleSettingsNotificationsController extends Controller {
/**
* Inject the notifications service.
*
* @memberof ConsoleSettingsNotificationsController
*/
@service notifications;
/**
* Inject the fetch service.
*
* @memberof ConsoleSettingsNotificationsController
*/
@service fetch;
/**
* The notification settings value JSON.
*
* @memberof ConsoleSettingsNotificationsController
* @var {Object}
*/
@service store;
@service currentUser;
@tracked notificationSettings = {};
/**
* Notification transport methods enabled.
*
* @memberof ConsoleSettingsNotificationsController
* @var {Array}
*/
@tracked notificationTransportMethods = ['email', 'sms'];
@tracked company;
/**
* Creates an instance of ConsoleSettingsNotificationsController.
@@ -45,6 +23,40 @@ export default class ConsoleSettingsNotificationsController extends Controller {
this.getSettings.perform();
}
/**
* Toggles the "Alphanumeric Sender ID" feature for the current company.
*
* Updates the company's `options` object by setting the
* `alpha_numeric_sender_id_enabled` flag. This controls whether the
* organization uses a custom alphanumeric sender ID when sending SMS.
*
* @action
* @param {boolean} enabled - Whether the feature should be enabled or disabled.
* @returns {void}
*/
@action toggleAlphaNumericSenderId(enabled) {
const currentOptions = this.company.options ?? {};
this.company.set('options', { ...currentOptions, alpha_numeric_sender_id_enabled: enabled });
}
/**
* Sets the Alphanumeric Sender ID string for the current company.
*
* Reads the input's value from the event and updates the company's `options`
* object by setting the `alpha_numeric_sender_id` field. This value represents
* the sender name that will appear in outbound SMS messages (subject to carrier
* support and restrictions).
*
* @action
* @param {Event} event - Input event containing the alphanumeric sender ID value.
* @returns {void}
*/
@action setAlphaNumericSenderId(event) {
const value = event.target.value;
const currentOptions = this.company.options ?? {};
this.company.set('options', { ...currentOptions, alpha_numeric_sender_id: value });
}
/**
* Selectes notifiables for settings.
*
@@ -94,7 +106,8 @@ export default class ConsoleSettingsNotificationsController extends Controller {
const { notificationSettings } = this;
try {
yield this.fetch.post('notifications/save-settings', { notificationSettings });
yield this.fetch.post('notifications/save-settings', { notificationSettings: notificationSettings ?? {} });
yield this.saveCompanyOptions.perform();
this.notifications.success('Notification settings successfully saved.');
} catch (error) {
this.notifications.serverError(error);
@@ -114,4 +127,26 @@ export default class ConsoleSettingsNotificationsController extends Controller {
this.notifications.serverError(error);
}
}
/**
* Saves the updated company options to the backend.
*
* This ember-concurrency task attempts to persist the company's modified
* `options` object by calling `company.save()`. If the request fails, a server
* error notification is displayed. No action is taken if no company is loaded.
*
* @task
* @generator
* @yields {Promise} Resolves when the save request completes.
* @returns {Promise<void>} Task completion state.
*/
@task *saveCompanyOptions() {
if (!this.company) return;
try {
yield this.company.save();
} catch (error) {
this.notifications.serverError(error);
}
}
}

View File

@@ -0,0 +1,9 @@
import setupDeprecationWorkflow from 'ember-cli-deprecation-workflow';
setupDeprecationWorkflow({
workflow: [
{ handler: 'silence', matchId: 'ember-concurrency.deprecate-decorator-task' },
{ handler: 'silence', matchId: 'new-helper-names' },
{ handler: 'silence', matchId: 'ember-data:deprecate-non-strict-relationships' },
],
});

View File

@@ -10,9 +10,11 @@
{{content-for "head"}}
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/favicon/android-chrome-192x192.png" />
<link rel="icon" type="image/png" sizes="256x256" href="/favicon/android-chrome-256x256.png" />
<link rel="manifest" href="/favicon/site.webmanifest" />
<link rel="mask-icon" href="/favicon/safari-pinned-tab.svg" color="#5bbad5" />
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/vendor.css">

View File

@@ -21,8 +21,7 @@ export function initialize(appInstance) {
debug('[Initializing Router Patch] Applying router refresh bug fix...');
try {
const application = appInstance.application;
applyRouterFix(application);
applyRouterFix(appInstance);
const endTime = performance.now();
debug(`[Initializing Router Patch] Router fix applied in ${(endTime - startTime).toFixed(2)}ms`);

View File

@@ -6,12 +6,10 @@ export async function initialize(appInstance) {
const application = appInstance.application;
const extensionManager = appInstance.lookup('service:universe/extension-manager');
if (!application.extensions || application.extensions.length === 0) {
try {
await extensionManager.loadExtensions(application);
} catch (error) {
console.error('[load-extensions] Error:', error);
}
try {
await extensionManager.loadExtensions(application);
} catch (error) {
console.error('[load-extensions] Error:', error);
}
}

View File

@@ -1,10 +1,4 @@
export function initialize(appInstance) {
// Set window.Fleetbase to the application instance for global access
// This is used by services and engines to access the root application instance
if (typeof window !== 'undefined') {
window.Fleetbase = appInstance;
}
// Look up UniverseService and set the application instance
const universeService = appInstance.lookup('service:universe');
if (universeService) {

View File

@@ -24,7 +24,7 @@ export default class Company extends Model {
@attr('string') logo_url;
@attr('string') backdrop_url;
@attr('string') description;
@attr('raw') options;
@attr('object') options;
@attr('number') users_count;
@attr('string') type;
@attr('string') currency;
@@ -33,6 +33,7 @@ export default class Company extends Model {
@attr('string') phone;
@attr('string') status;
@attr('string') slug;
@attr('boolean', { defaultValue: false }) onboarding_completed;
/** @dates */
@attr('date') joined_at;

View File

@@ -34,6 +34,7 @@ export default class UserModel extends Model {
@attr('boolean') is_admin;
@attr('boolean') is_subscribed;
@attr('boolean') is_trialing;
@attr('boolean') company_onboarding_completed;
@attr('raw') meta;
@attr('raw') subscription;

View File

@@ -5,6 +5,7 @@ import groupBy from '@fleetbase/ember-core/utils/group-by';
export default class ConsoleSettingsNotificationsRoute extends Route {
@service fetch;
@service currentUser;
model() {
return hash({
@@ -13,10 +14,11 @@ export default class ConsoleSettingsNotificationsRoute extends Route {
});
}
setupController(controller, { registry, notifiables }) {
async setupController(controller, { registry, notifiables }) {
super.setupController(...arguments);
controller.groupedNotifications = groupBy(registry, 'package');
controller.notifiables = notifiables;
controller.company = await this.currentUser.loadCompany();
}
}

View File

@@ -12,7 +12,8 @@ export default class OnboardIndexRoute extends Route {
};
beforeModel() {
this.orchestrator.start();
// Resume from previous session if data exists in localStorage
this.orchestrator.start(null, { resume: true });
}
model() {

View File

@@ -4,8 +4,6 @@ import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
export default class UserSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
/**
* Embedded relationship attributes
*
* @var {Object}
*/
get attrs() {
return {
@@ -16,22 +14,45 @@ export default class UserSerializer extends ApplicationSerializer.extend(Embedde
}
/**
* Customize serializer so that the password is never sent to the server via Ember Data
* Prevent partial payloads from overwriting fully-loaded
* user records in the store.
*
* @param {Snapshot} snapshot
* @param {Object} options
* @return {Object} json
* This runs ONLY on incoming data.
*/
normalize(modelClass, resourceHash, prop) {
let normalized = super.normalize(modelClass, resourceHash, prop);
// Existing user already loaded in the store?
let existing = this.store.peekRecord(normalized.data.type, normalized.data.id);
if (existing) {
let attrs = normalized.data.attributes || {};
for (let key in attrs) {
if (attrs[key] === null || attrs[key] === undefined || key === 'avatar_url') {
delete attrs[key];
}
}
}
return normalized;
}
/**
* Customize serializer so that sensitive or server-managed
* fields are never sent to the backend.
*/
serialize() {
const json = super.serialize(...arguments);
// delete the password always
// Never send password
delete json.password;
// delete verification attributes
// Verification flags
delete json.email_verified_at;
delete json.phone_verified_at;
// delete server managed dates
// Server-managed timestamps
delete json.deleted_at;
delete json.created_at;
delete json.updated_at;

View File

@@ -1,39 +1,133 @@
import Service, { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
const CONTEXT_PREFIX = 'onboarding:context:';
const KEYS_INDEX = `${CONTEXT_PREFIX}__keys__`;
export default class OnboardingContextService extends Service {
@service appCache;
@tracked data = {};
/**
* Get a value from in-memory state first, then fallback to cache
*/
get(key) {
return this.data[key] ?? this.appCache.get(`onboarding:context:${key}`);
return this.data[key] ?? this.appCache.get(`${CONTEXT_PREFIX}${key}`);
}
/**
* Get a value directly from cache
*/
getFromCache(key) {
return this.appCache.get(`onboarding:context:${key}`);
return this.appCache.get(`${CONTEXT_PREFIX}${key}`);
}
set(key, value, options = {}) {
this.data = { ...this.data, [key]: value };
if (options?.persist === true) {
this.appCache.set(`onboarding:context:${key}`, value);
/**
* Restore all persisted onboarding context values from cache
*
* @returns {Object}
*/
restore() {
const keys = this.appCache.get(KEYS_INDEX) ?? [];
const persisted = {};
for (const key of keys) {
const value = this.appCache.get(`${CONTEXT_PREFIX}${key}`);
if (value !== undefined) {
persisted[key] = value;
}
}
return persisted;
}
/**
* Merge data into the context
* Optionally persist all merged values
*/
merge(data = {}, options = {}) {
if (!data || typeof data !== 'object') {
return;
}
// Filter out sensitive fields
const sensitiveFields = ['password', 'password_confirmation'];
const filteredData = {};
for (const [key, value] of Object.entries(data)) {
if (!sensitiveFields.includes(key)) {
filteredData[key] = value;
}
}
this.data = { ...this.data, ...filteredData };
if (options.persist === true) {
const keys = new Set(this.appCache.get(KEYS_INDEX) ?? []);
for (const key of Object.keys(filteredData)) {
keys.add(key);
this.appCache.set(`${CONTEXT_PREFIX}${key}`, this.data[key]);
}
this.appCache.set(KEYS_INDEX, [...keys]);
}
}
/**
* Set a single value
* Optionally persist it
*/
set(key, value, options = {}) {
// Don't store sensitive fields
const sensitiveFields = ['password', 'password_confirmation'];
if (sensitiveFields.includes(key)) {
return;
}
this.data = { ...this.data, [key]: value };
if (options.persist === true) {
const keys = new Set(this.appCache.get(KEYS_INDEX) ?? []);
keys.add(key);
this.appCache.set(`${CONTEXT_PREFIX}${key}`, value);
this.appCache.set(KEYS_INDEX, [...keys]);
}
}
/**
* Convenience alias for persisted set
*/
persist(key, value) {
this.set(key, value, { persist: true });
}
/**
* Delete a key from memory and cache
*/
del(key) {
const { [key]: _drop, ...rest } = this.data; // eslint-disable-line no-unused-vars
const { [key]: _removed, ...rest } = this.data; // eslint-disable-line no-unused-vars
this.data = rest;
this.appCache.set(`onboarding:context:${key}`, undefined);
const keys = new Set(this.appCache.get(KEYS_INDEX) ?? []);
keys.delete(key);
this.appCache.set(`${CONTEXT_PREFIX}${key}`, undefined);
this.appCache.set(KEYS_INDEX, [...keys]);
}
/**
* Fully reset onboarding context (memory + persistence)
*/
reset() {
for (let key in this.data) {
this.appCache.set(`onboarding:context:${key}`, undefined);
const keys = this.appCache.get(KEYS_INDEX) ?? [];
for (const key of keys) {
this.appCache.set(`${CONTEXT_PREFIX}${key}`, undefined);
}
this.appCache.set(KEYS_INDEX, []);
this.data = {};
}
}
}

View File

@@ -7,17 +7,43 @@ export default class OnboardingOrchestratorService extends Service {
@service onboardingContext;
@tracked flow = null;
@tracked wrapper = null;
@tracked current = null;
@tracked history = [];
@tracked sessionId = null;
start(flowId = null, opts = {}) {
/**
* localStorage key for persisting navigation history
*/
get historyStorageKey() {
return `onboarding:history:${this.flow?.id || 'default'}`;
}
async start(flowId = null, opts = {}) {
const flow = this.onboardingRegistry.getFlow(flowId ?? this.onboardingRegistry.defaultFlow);
if (!flow) throw new Error(`Onboarding flow '${flowId}' not found`);
this.flow = flow;
this.wrapper = flow.wrapper || null;
this.sessionId = opts.sessionId || null;
this.history = [];
this.goto(flow.entry);
// Restore history if resuming from a previous session
if (opts.resume) {
this._restoreHistory();
}
// Execute onFlowWillStart hook if defined
if (typeof this.flow.onFlowWillStart === 'function') {
await this.flow.onFlowWillStart(this.flow, this);
}
await this.goto(flow.entry);
// Execute onFlowDidStart hook if defined
if (typeof this.flow.onFlowDidStart === 'function') {
await this.flow.onFlowDidStart(this.flow, this);
}
}
async goto(stepId) {
@@ -25,27 +51,46 @@ export default class OnboardingOrchestratorService extends Service {
const step = this.flow.steps.find((s) => s.id === stepId);
if (!step) throw new Error(`Step '${stepId}' not found`);
// Execute onStepWillChange hook if defined
const previousStep = this.current;
if (typeof this.flow.onStepWillChange === 'function') {
await this.flow.onStepWillChange(step, previousStep, this);
}
// Guard function - skip step if guard returns false
if (typeof step.guard === 'function' && !step.guard(this.onboardingContext)) {
return this.next();
}
// beforeEnter lifecycle hook
if (typeof step.beforeEnter === 'function') {
await step.beforeEnter(this.onboardingContext);
}
this.current = step;
// Execute onStepDidChange hook if defined
if (typeof this.flow.onStepDidChange === 'function') {
await this.flow.onStepDidChange(this.current, previousStep, this);
}
}
async next() {
if (!this.flow || !this.current) return;
const leaving = this.current;
// afterLeave lifecycle hook
if (typeof leaving.afterLeave === 'function') {
await leaving.afterLeave(this.onboardingContext);
}
if (!this.history.includes(leaving)) this.history.push(leaving);
if (!this.history.includes(leaving)) {
this.history.push(leaving);
this._persistHistory();
}
// Support both string and function for next property
let nextId;
if (typeof leaving.next === 'function') {
nextId = leaving.next(this.onboardingContext);
@@ -53,8 +98,23 @@ export default class OnboardingOrchestratorService extends Service {
nextId = leaving.next;
}
// If no next step, flow is complete
if (!nextId) {
// Execute onFlowWillEnd hook if defined
if (typeof this.flow.onFlowWillEnd === 'function') {
await this.flow.onFlowWillEnd(leaving, this);
}
this.current = null; // finished
// Execute onFlowDidEnd hook if defined
if (typeof this.flow.onFlowDidEnd === 'function') {
await this.flow.onFlowDidEnd(leaving, this);
}
// Clear history from localStorage when flow completes
this._clearHistory();
return;
}
@@ -66,6 +126,89 @@ export default class OnboardingOrchestratorService extends Service {
const prev = this.history[this.history.length - 1];
if (prev && prev.allowBack === false) return;
this.history = this.history.slice(0, -1);
this._persistHistory();
await this.goto(prev.id);
}
/**
* Get the current path (for flows with multiple paths)
* This is a helper method that can be used by flows to determine the current path
*/
getCurrentPath() {
if (!this.flow || !this.flow.paths) return null;
// Determine path based on context or current step
for (const [pathId, pathDef] of Object.entries(this.flow.paths)) {
if (pathDef.steps && pathDef.steps.some(s => s.id === this.current?.id)) {
return pathDef;
}
}
return null;
}
/**
* Check if a step is in the current path
*/
isStepInPath(stepId) {
const currentPath = this.getCurrentPath();
if (!currentPath) return true; // If no paths defined, all steps are valid
return currentPath.steps?.some(s => s.id === stepId) ?? false;
}
/**
* Persist navigation history to localStorage
* Stores only step IDs to keep storage lightweight
* @private
*/
_persistHistory() {
if (!this.flow) return;
try {
const historyIds = this.history.map(step => step.id);
localStorage.setItem(this.historyStorageKey, JSON.stringify(historyIds));
} catch (error) {
console.warn('[OnboardingOrchestrator] Failed to persist history:', error);
}
}
/**
* Restore navigation history from localStorage
* Reconstructs step objects from stored IDs
* @private
*/
_restoreHistory() {
if (!this.flow) return;
try {
const stored = localStorage.getItem(this.historyStorageKey);
if (!stored) return;
const historyIds = JSON.parse(stored);
this.history = historyIds
.map(id => this.flow.steps.find(s => s.id === id))
.filter(Boolean); // Remove any invalid steps
console.log('[OnboardingOrchestrator] Restored history:', this.history.map(s => s.id));
} catch (error) {
console.warn('[OnboardingOrchestrator] Failed to restore history:', error);
this.history = [];
}
}
/**
* Clear navigation history from localStorage
* Called when flow completes or is reset
* @private
*/
_clearHistory() {
if (!this.flow) return;
try {
localStorage.removeItem(this.historyStorageKey);
} catch (error) {
console.warn('[OnboardingOrchestrator] Failed to clear history:', error);
}
}
}

View File

@@ -9,7 +9,7 @@ export default class OnboardingRegistryService extends Service {
this.defaultFlow = flowId;
}
registerFlow(flow) {
registerFlow(flow, options = {}) {
if (!flow || !flow.id || !flow.entry || !Array.isArray(flow.steps)) {
throw new Error('Invalid FlowDef: id, entry, steps are required');
}
@@ -23,6 +23,11 @@ export default class OnboardingRegistryService extends Service {
}
}
this.flows.set(flow.id, flow);
// If specified, set as default flow
if (options.default) {
this.defaultFlow = flow.id;
}
}
getFlow(id) {

View File

@@ -4,21 +4,27 @@
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto">
<ContentPanel @title={{t "common.your-profile"}} @open={{true}} @wrapperClass="bordered-classic">
<form class="flex flex-col md:flex-row" {{on "submit" (perform this.saveProfile)}}>
<form class="flex flex-col items-start md:flex-row" {{on "submit" (perform this.saveProfile)}}>
<div class="w-32 flex flex-col justify-center mb-6 mr-6">
<Image src={{this.user.avatar_url}} @fallbackSrc={{config "defaultValues.userImage"}} alt={{this.user.name}} class="w-32 h-32 rounded-md" />
<FileUpload @name={{t "console.account.index.photos"}} @accept="image/*" @onFileAdded={{this.uploadNewPhoto}} @labelClass="flex flex-row items-center justify-center" as |queue|>
<Image src={{this.user.avatar_url}} @fallbackSrc={{config "defaultValues.userImage"}} alt={{this.user.name}} class="w-32 h-32 rounded-md mt-1" />
<FileUpload
@name={{t "console.account.index.photos"}}
@accept="image/*"
@onFileAdded={{this.uploadNewPhoto}}
@labelClass="flex flex-row items-center justify-center"
as |queue|
>
<a tabindex={{0}} class="flex items-center px-0 mt-2 text-xs no-underline truncate btn btn-sm btn-default" disabled={{queue.files.length}}>
{{#if queue.files.length}}
<div class="mr-1.5">
<Spinner />
</div>
<span>
{{t "common.uploading"}}
{{t "common.uploading"}}
</span>
{{else}}
<FaIcon @icon="image" class="mr-1.5" />
<span>
<span>
{{t "console.account.index.upload-new"}}
</span>
{{/if}}
@@ -34,11 +40,31 @@
</InputGroup>
<InputGroup @name={{t "common.date-of-birth"}} @type="date" @value={{this.user.date_of_birth}} />
<InputGroup @name={{t "common.timezone"}} @helpText={{t "console.account.index.timezone"}}>
<Select @value={{this.user.timezone}} @options={{this.timezones}} @onSelect={{fn (mut this.user.timezone)}} @placeholder={{t "console.account.index.timezone"}} />
<div class="fleetbase-model-select fleetbase-power-select ember-model-select">
<PowerSelect
@options={{this.timezones}}
@selected={{this.user.timezone}}
@onChange={{fn (mut this.user.timezone)}}
@placeholder={{t "console.account.index.timezone"}}
@triggerClass="form-select form-input"
@searchEnabled={{true}}
as |option|
>
<div>{{option}}</div>
</PowerSelect>
</div>
</InputGroup>
</div>
<div class="mt-3 flex items-center justify-end">
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text={{t "common.save-changes"}} @onClick={{perform this.saveProfile}} @isLoading={{not this.saveProfile.isIdle}} />
<Button
@buttonType="submit"
@type="primary"
@size="lg"
@icon="save"
@text={{t "common.save-changes"}}
@onClick={{perform this.saveProfile}}
@isLoading={{not this.saveProfile.isIdle}}
/>
</div>
</div>
</form>

View File

@@ -1,10 +1,10 @@
{{page-title @model.title}}
<Layout::Section::Header @title={{@model.title}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-">
<LazyEngineComponent @component={{@model.component}} @params={{@model.componentParams}} />
<Layout::Section::Body class={{this.bodyClass}}>
<div class={{this.containerClass}}>
<div class={{this.wrapperClass}}>
{{component (lazy-engine-component @model.component) params=@model.componentParams controller=this}}
</div>
</div>
<Spacer @height="300px" />

View File

@@ -17,7 +17,7 @@
@selected={{get this.notificationSettings (concat (get-notification-key notification.definition notification.name) ".notifiables")}}
@onChange={{fn this.onSelectNotifiable notification}}
@placeholder="Select notifiables..."
@triggerClass="form-select form-input form-input-sm flex-1"
@triggerClass="form-select form-input flex-1"
as |notifiable|
>
{{notifiable.label}}
@@ -27,6 +27,21 @@
{{/each}}
</ContentPanel>
{{/each-in}}
<ContentPanel @title="SMS Notification Settings" @open={{true}} @wrapperClass="bordered-classic">
<Toggle @isToggled={{this.company.options.alpha_numeric_sender_id_enabled}} @onToggle={{this.toggleAlphaNumericSenderId}} @label="Enable Alpha-Numeric Sender ID" @wrapperClass="mb-4" />
<InputGroup @name="Alpha-Numeric Sender ID" @value={{this.company.options.alpha_numeric_sender_id}} @helpText="Set the custom alphanumeric name that will appear as the sender for all SMS sent by your organization. Up to 11 letters or numbers. Not supported in all countries." @disabled={{not this.company.options.alpha_numeric_sender_id_enabled}} />
<div class="space-y-2 mb-3">
<InfoBlock>
<p>Alphanumeric Sender IDs allow your organization to replace a traditional phone number with a custom text-based sender name when sending SMS notifications (e.g., Fleetbase, MyStore, DispatchHQ). This can improve brand recognition, increase message trust, and enhance deliverability in regions where numeric senders are restricted by local carriers.</p>
<p>When enabled, Fleetbase will use this sender ID for all outbound SMS messages sent on behalf of your organization, including order updates, verification codes, driver notifications, and other automated alerts. Sender IDs can contain up to 11 characters using letters and numbers.</p>
<p>Some countries require or enforce specific messaging rules, and certain carriers may only support alphanumeric senders. Using a Sender ID can significantly improve message delivery in these regions.</p>
</InfoBlock>
<InfoBlock @type="warning">
<p>Delivery of SMS using Alphanumeric Sender IDs depends on local carrier policies. Some regions may restrict or block numeric senders or require the use of alphanumeric senders for successful delivery. While Fleetbase will attempt to deliver all messages using your configured Sender ID, message delivery cannot be guaranteed in countries with carrier-level filtering or regulatory restrictions. Your organization is responsible for ensuring compliance with local messaging regulations in the countries you send SMS to.</p>
</InfoBlock>
</div>
</ContentPanel>
</div>
</div>
<Spacer @height="300px" />

View File

@@ -1,6 +1,3 @@
<div class="flex items-center justify-center h-screen min-h-screen px-4 py-12 bg-gray-50 dark:bg-gray-900 sm:px-6 lg:px-8 overflow-y-scroll">
<div class="w-full max-w-md h-screen flex items-center justify-center py-4">
{{outlet}}
</div>
<Spacer @height="300px" />
<div class="onboard-route-wrapper">
{{outlet}}
</div>

View File

@@ -79,6 +79,12 @@ function getCachedConfig() {
return null;
}
// Application version has changed
if (cachedVersion !== config.APP.version) {
debug(`[Runtime Config] Version mismatch (cached: ${cachedVersion}, current: ${config.APP.version})`);
return null;
}
const cacheData = JSON.parse(cached);
const cacheAge = Date.now() - cacheData.timestamp;
@@ -101,14 +107,14 @@ function getCachedConfig() {
*
* @param {Object} config Config object
*/
function setCachedConfig(config) {
function setCachedConfig(runtimeConfig) {
try {
const cacheData = {
config,
config: runtimeConfig,
timestamp: Date.now(),
};
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
localStorage.setItem(CACHE_VERSION_KEY, '1');
localStorage.setItem(CACHE_VERSION_KEY, config.APP.version);
debug('[Runtime Config] Config cached to localStorage');
} catch (e) {
debug(`[Runtime Config] Failed to cache config: ${e.message}`);
@@ -147,12 +153,15 @@ export default async function loadRuntimeConfig() {
return;
}
// // Try cache first
// const cachedConfig = getCachedConfig();
// if (cachedConfig) {
// applyRuntimeConfig(cachedConfig);
// return;
// }
const isProduction = config?.environment === 'production';
if (isProduction) {
// Try cache first
const cachedConfig = getCachedConfig();
if (cachedConfig) {
applyRuntimeConfig(cachedConfig);
return;
}
}
// Cache miss - fetch from server
try {

View File

@@ -23,7 +23,7 @@ module.exports = function (environment) {
APP: {
autoboot: true,
extensions: asArray(getenv('EXTENSIONS')),
disableRuntimeConfig: toBoolean(getenv('DISABLE_RUNTIME_CONFIG')),
disableRuntimeConfig: toBoolean(getenv('DISABLE_RUNTIME_CONFIG', environment === 'production')),
},
API: {

View File

@@ -5,16 +5,7 @@ const EmberApp = require('ember-cli/lib/broccoli/ember-app');
const Funnel = require('broccoli-funnel');
const writeFile = require('broccoli-file-creator');
const mergeTrees = require('broccoli-merge-trees');
const postcssImport = require('postcss-import');
const postcssPresetEnv = require('postcss-preset-env');
const postcssEach = require('postcss-each');
const postcssMixins = require('postcss-mixins');
const postcssConditionals = require('postcss-conditionals-renewed');
const postcssAtRulesVariables = require('postcss-at-rules-variables');
const autoprefixer = require('autoprefixer');
const tailwind = require('tailwindcss');
const toBoolean = require('./config/utils/to-boolean');
const environment = process.env.EMBER_ENV;
module.exports = function (defaults) {
const app = new EmberApp(defaults, {
@@ -38,29 +29,6 @@ module.exports = function (defaults) {
useSessionSetupMethod: true,
},
postcssOptions: {
compile: {
enabled: true,
cacheInclude: [/.*\.(css|scss|hbs)$/, /.*\/tailwind\/config\.js$/, /.*tailwind\.js$/],
plugins: [
postcssAtRulesVariables,
postcssImport({
path: ['node_modules'],
plugins: [postcssAtRulesVariables, postcssImport],
}),
postcssMixins,
postcssPresetEnv({ stage: 1 }),
postcssEach,
tailwind('./tailwind.config.js'),
autoprefixer,
],
},
filter: {
enabled: true,
plugins: [postcssAtRulesVariables, postcssMixins, postcssEach, postcssConditionals, tailwind('./tailwind.config.js')],
},
},
babel: {
plugins: [require.resolve('ember-auto-import/babel-plugin')],
},

View File

@@ -1,6 +1,6 @@
{
"name": "@fleetbase/console",
"version": "0.7.20",
"version": "0.7.26",
"private": true,
"description": "Modular logistics and supply chain operating system (LSOS)",
"repository": "https://github.com/fleetbase/fleetbase",
@@ -33,15 +33,15 @@
},
"dependencies": {
"@ember/legacy-built-in-components": "^0.4.2",
"@fleetbase/dev-engine": "^0.2.11",
"@fleetbase/ember-core": "^0.3.7",
"@fleetbase/ember-ui": "^0.3.12",
"@fleetbase/fleetops-data": "^0.1.23",
"@fleetbase/fleetops-engine": "^0.6.28",
"@fleetbase/iam-engine": "^0.1.5",
"@fleetbase/dev-engine": "^0.2.12",
"@fleetbase/ember-core": "^0.3.10",
"@fleetbase/ember-ui": "^0.3.18",
"@fleetbase/fleetops-data": "^0.1.25",
"@fleetbase/fleetops-engine": "^0.6.33",
"@fleetbase/iam-engine": "^0.1.6",
"@fleetbase/leaflet-routing-machine": "^3.2.17",
"@fleetbase/registry-bridge-engine": "^0.1.1",
"@fleetbase/storefront-engine": "^0.4.8",
"@fleetbase/registry-bridge-engine": "^0.1.3",
"@fleetbase/storefront-engine": "^0.4.12",
"@formatjs/intl-datetimeformat": "^6.18.2",
"@formatjs/intl-numberformat": "^8.15.6",
"@formatjs/intl-pluralrules": "^5.4.6",
@@ -82,6 +82,8 @@
"broccoli-asset-rev": "^3.0.0",
"broccoli-file-creator": "^2.1.1",
"broccoli-funnel": "^3.0.8",
"broccoli-merge-trees": "^4.2.0",
"chokidar": "4.0.3",
"concurrently": "^8.2.2",
"date-fns": "^2.30.0",
"dragula": "^3.7.3",
@@ -91,6 +93,7 @@
"ember-cli-babel": "^8.2.0",
"ember-cli-clean-css": "^3.0.0",
"ember-cli-dependency-checker": "^3.3.2",
"ember-cli-deprecation-workflow": "^4.0.0",
"ember-cli-dotenv": "^3.1.0",
"ember-cli-htmlbars": "^6.3.0",
"ember-cli-inject-live-reload": "^2.1.0",
@@ -147,9 +150,9 @@
},
"pnpm": {
"overrides": {
"@fleetbase/ember-core": "latest",
"@fleetbase/ember-ui": "latest",
"@fleetbase/fleetops-data": "latest"
"@fleetbase/ember-core": "^0.3.10",
"@fleetbase/ember-ui": "^0.3.18",
"@fleetbase/fleetops-data": "^0.1.24"
}
},
"prettier": {
@@ -166,5 +169,6 @@
}
}
]
}
},
"packageManager": "pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903"
}

1315
console/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,6 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/favicon/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/favicon/android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
}
],
"name": "Fleetbase Console",
"short_name": "Fleetbase",
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"

View File

@@ -1,26 +0,0 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | dashboard/widget-panel', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.set('myAction', function(val) { ... });
await render(hbs`<Dashboard::WidgetPanel />`);
assert.dom().hasText('');
// Template block usage:
await render(hbs`
<Dashboard::WidgetPanel>
template block text
</Dashboard::WidgetPanel>
`);
assert.dom().hasText('template block text');
});
});

View File

@@ -346,6 +346,7 @@ common:
{resource} ({resourceName}) deleted.
continue-without-saving: Continue Without Saving?
continue-without-saving-prompt: You have unsaved changes to this {resource}. Continuing will discard them. Click Continue to proceed.
changelog: Changelog
resource:
alert: Alert

View File

@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
# Base stage
FROM dunglas/frankenphp:1.5.0-php8.2-bookworm AS base
FROM dunglas/frankenphp:1.11-php8.2-bookworm AS base
# Install packages
RUN apt-get update && apt-get install -y git bind9-utils mycli nodejs npm nano uuid-runtime \
@@ -75,7 +75,7 @@ ENV QUEUE_CONNECTION=redis
ENV CADDYFILE_PATH=/fleetbase/Caddyfile
ENV CONSOLE_PATH=/fleetbase/console
ENV OCTANE_SERVER=frankenphp
ENV FLEETBASE_VERSION=0.7.20
ENV FLEETBASE_VERSION=0.7.26
# Set environment
ARG ENVIRONMENT=production
@@ -158,14 +158,14 @@ CMD ["php", "artisan", "queue:work"]
# Application dev stage
FROM base AS app-dev
ENTRYPOINT ["docker-php-entrypoint"]
CMD ["sh", "-c", "php artisan octane:frankenphp --max-requests=250 --port=8000 --host=0.0.0.0 --watch"]
CMD ["sh", "-c", "php artisan octane:frankenphp --max-requests=1000 --port=8000 --host=0.0.0.0 --watch"]
# Application release stage
FROM base AS app-release
ENTRYPOINT ["docker-php-entrypoint"]
CMD ["sh", "-c", "php artisan octane:frankenphp --max-requests=250 --port=8000 --host=0.0.0.0"]
CMD ["sh", "-c", "php artisan octane:frankenphp --max-requests=1000 --port=8000 --host=0.0.0.0"]
# Application stage
FROM base AS app
ENTRYPOINT ["/sbin/ssm-parent", "-c", ".ssm-parent.yaml", "run", "--", "docker-php-entrypoint"]
CMD ["sh", "-c", "php artisan octane:frankenphp --max-requests=250 --port=8000 --host=0.0.0.0"]
CMD ["sh", "-c", "php artisan octane:frankenphp --max-requests=1000 --port=8000 --host=0.0.0.0"]