mirror of
https://github.com/fleetbase/fleetbase.git
synced 2026-01-05 22:05:50 +00:00
Compare commits
83 Commits
dev-v0.7.1
...
v0.7.23
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53a87d6f38 | ||
|
|
d7f8f87315 | ||
|
|
36673ef564 | ||
|
|
19341c81e7 | ||
|
|
b4ecf5bda9 | ||
|
|
1b714a7ef8 | ||
|
|
e41cd62ea5 | ||
|
|
1ca1342052 | ||
|
|
a5a5ddb0d5 | ||
|
|
c51f3ca6c8 | ||
|
|
a9b172081a | ||
|
|
a29ca0ecb9 | ||
|
|
6442644438 | ||
|
|
0238632fdd | ||
|
|
27652db9c3 | ||
|
|
5eaf2039d4 | ||
|
|
d8c06ae0be | ||
|
|
c9011b3ffa | ||
|
|
520de0f6bc | ||
|
|
ba6ed235e3 | ||
|
|
777d84a7fe | ||
|
|
18fdfdf506 | ||
|
|
7e898dd54b | ||
|
|
761c752a8e | ||
|
|
74d02efaa0 | ||
|
|
aa928b43ba | ||
|
|
7ed422d893 | ||
|
|
064fa12a43 | ||
|
|
32f0b22ed1 | ||
|
|
4f87434911 | ||
|
|
498d519c49 | ||
|
|
e4b008093d | ||
|
|
c31377b194 | ||
|
|
6f397ea3cb | ||
|
|
015c24585b | ||
|
|
1f4b25faee | ||
|
|
3a193e414c | ||
|
|
9719632289 | ||
|
|
08dabaf138 | ||
|
|
7ae3ea95a2 | ||
|
|
9653cfcaf0 | ||
|
|
cb7a2fb05b | ||
|
|
fd569cfeaf | ||
|
|
0f9cd52bb4 | ||
|
|
72ce000786 | ||
|
|
c9477b78f2 | ||
|
|
affa141c9d | ||
|
|
5726eb974f | ||
|
|
ca3050905d | ||
|
|
cf2ced1512 | ||
|
|
93b7224335 | ||
|
|
9a053cfd9f | ||
|
|
56897af057 | ||
|
|
b27e485a44 | ||
|
|
94c5407387 | ||
|
|
54ac27b304 | ||
|
|
4fb596c866 | ||
|
|
3f12e98448 | ||
|
|
a1fc1e4ff8 | ||
|
|
d622b617c3 | ||
|
|
edba6c8396 | ||
|
|
a0fc1ce402 | ||
|
|
ffab66ac6c | ||
|
|
d3555c7c82 | ||
|
|
4b12efef41 | ||
|
|
c80f507720 | ||
|
|
2da7ee9c19 | ||
|
|
658568e4ec | ||
|
|
8a487b2352 | ||
|
|
bc89218a26 | ||
|
|
5a4f7e2ae3 | ||
|
|
9fa1bf54d2 | ||
|
|
13cfe00958 | ||
|
|
6cab778f93 | ||
|
|
b98eb3adf5 | ||
|
|
5473b50c40 | ||
|
|
d9f415528e | ||
|
|
76b0bfbfcd | ||
|
|
0432003163 | ||
|
|
7cb4654c86 | ||
|
|
b9adb92fc1 | ||
|
|
d81bd4e900 | ||
|
|
8a21593d9a |
@@ -8,6 +8,7 @@
|
||||
http://:8000 {
|
||||
root * /fleetbase/api/public
|
||||
encode zstd br gzip
|
||||
|
||||
php_server {
|
||||
resolve_root_symlink
|
||||
}
|
||||
|
||||
14
README.md
14
README.md
@@ -125,10 +125,10 @@ Next copy this value to the `APP_KEY` environment variable in the application co
|
||||
|
||||
**Routing:** Fleetbase ships with a default OSRM server hosted by `[router.project-osrm.org](https://router.project-osrm.org)` but you’re able to use your own or any other OSRM compatible server. You can modify this in the `console/environments` directory by modifying the .env file of the environment you’re deploying and setting the `OSRM_HOST` to the OSRM server for Fleetbase to use.
|
||||
|
||||
**Services:** There are a few environment variables which need to be set for Fleetbase to function with full features. If you’re deploying with docker then it’s easiest to just create a `docker-compose.override.yml` and supply the environment variables in this file.
|
||||
**Services:** There are a few environment variables which need to be set for Fleetbase to function with full features. If you're deploying with docker then it's easiest to just create a `docker-compose.override.yml` and supply the environment variables in this file.
|
||||
|
||||
```yaml
|
||||
version: “3.8”
|
||||
version: "3.8"
|
||||
services:
|
||||
application:
|
||||
environment:
|
||||
@@ -141,8 +141,18 @@ services:
|
||||
TWILIO_SID:
|
||||
TWILIO_TOKEN:
|
||||
TWILIO_FROM:
|
||||
|
||||
socket:
|
||||
environment:
|
||||
# IMPORTANT: Configure WebSocket origins for security
|
||||
# Development (localhost only - include WebSocket protocols):
|
||||
SOCKETCLUSTER_OPTIONS: '{"origins":"http://localhost:*,https://localhost:*,ws://localhost:*,wss://localhost:*"}'
|
||||
# Production (replace with your actual domain):
|
||||
# SOCKETCLUSTER_OPTIONS: '{"origins":"https://yourdomain.com:*,wss://yourdomain.com:*"}'
|
||||
```
|
||||
|
||||
**WebSocket Security:** The `SOCKETCLUSTER_OPTIONS` environment variable controls which domains can connect to the WebSocket server. Always restrict origins to your specific domains in production to prevent security vulnerabilities.
|
||||
|
||||
You can learn more about full installation, and configuration in the [official documentation](https://docs.fleetbase.io/getting-started/install).
|
||||
|
||||
## 🚀 Deploy on AWS in One Click
|
||||
|
||||
40
RELEASE.md
40
RELEASE.md
@@ -1,23 +1,45 @@
|
||||
# 🚀 Fleetbase v0.7.18 — 2025-11-10
|
||||
# 🚀 Fleetbase v0.7.23 — 2025-12-19
|
||||
|
||||
> "Hotfix IAM user validation, make online/offline toggle silent"
|
||||
> "🤯 Insane optimization and performance upgrades + horizontal scaling support 🚀"
|
||||
|
||||
---
|
||||
|
||||
## ✨ Highlights
|
||||
- Hotfix validateRequest implementation to not rewrite request params
|
||||
- Hotfix user validation password optional for creation
|
||||
- Made online/offline endpoint for drivers silent
|
||||
- Hotfix QPay payment gateway on Storefront + ebarimt reciept fix
|
||||
|
||||
- Major performance and optimization improvements which support horizontal scaling
|
||||
- Ability to resize images on upload using resize parameters
|
||||
- Several patches in FleetOps - fixed service rates and missing translations, improvements and patch to scheduler
|
||||
- Added a new `LanguageService` available in ember-core
|
||||
- Minor `@fleetbase/ember-ui` improvements
|
||||
|
||||
### New Features
|
||||
- **Improved API performance** with two-layer caching system (Redis + ETag validation) for user and organization data
|
||||
- **Reduced bandwidth usage** with automatic HTTP 304 Not Modified responses via new ValidateETag middleware
|
||||
- **Faster page loads** with intelligent cache invalidation that updates immediately when data changes
|
||||
- **New UserCacheService class** for centralized cache management across the application
|
||||
- **Image resizing support** for dynamic image dimensions via URL parameters
|
||||
- Added `ApiModelCache` class - Provides intelligent Redis-based caching for API query results with automatic invalidation
|
||||
- Added `HasApiModelCache` trait - Enables models to cache query results with a single method call
|
||||
|
||||
### Performance Improvements
|
||||
- Optimized form data syncing to eliminate N+1 query problems, reducing database queries from N to 2 for relationship syncing
|
||||
- Implemented cache stampede prevention to handle high concurrent load efficiently
|
||||
- Added cache versioning system for automatic invalidation when data changes
|
||||
|
||||
### Developer Experience
|
||||
- Added `X-Cache-Status` header to API responses for easy cache debugging (HIT/MISS visibility)
|
||||
- Automatic multi-tenant cache key generation for company-scoped data isolation
|
||||
- Graceful fallback to direct queries when cache is unavailable
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Breaking Changes
|
||||
- None
|
||||
- None 🙂
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Upgrade Steps
|
||||
|
||||
```bash
|
||||
# Pull latest version
|
||||
git pull origin main --no-rebase
|
||||
@@ -30,5 +52,7 @@ docker compose down && docker compose up -d
|
||||
docker compose exec application bash -c "./deploy.sh"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
|
||||
100
TRANSLATING.md
Normal file
100
TRANSLATING.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Contributing to Fleetbase Translations
|
||||
|
||||
First off, thank you for considering contributing to Fleetbase translations! Your efforts help make Fleetbase accessible to a global audience. This guide will walk you through the process of adding or updating language translations for the Fleetbase platform and its various extensions.
|
||||
|
||||
## Understanding the Structure
|
||||
|
||||
Fleetbase is a modular system. The main application, known as Fleetbase Console, has its own set of translations. Additionally, each extension (like FleetOps or Storefront) also contains its own translation files. This means that to provide a complete translation for a specific language, you may need to contribute to multiple repositories.
|
||||
|
||||
- **Main Application (`fleetbase/fleetbase`)**: Contains the core translation files for the Fleetbase Console.
|
||||
- **Extensions/Modules**: Each extension has its own repository and its own set of translation files.
|
||||
|
||||
## File Format and Location
|
||||
|
||||
All translation files are in the **YAML** format (`.yaml` or `.yml`). The base language for all translations is American English (`en-us.yaml`).
|
||||
|
||||
- In the main `fleetbase/fleetbase` repository, the translation files are located at `./console/translations/`.
|
||||
- In each extension repository, the translation files are located at `./translations/`.
|
||||
|
||||
Translation files are named using the language and region code, for example:
|
||||
|
||||
- `en-us.yaml` (American English)
|
||||
- `fr-fr.yaml` (French, France)
|
||||
- `zh-cn.yaml` (Chinese, Simplified)
|
||||
|
||||
## How to Contribute Translations
|
||||
|
||||
Follow these steps to contribute a new translation or update an existing one.
|
||||
|
||||
### Step 1: Fork and Clone the Repository
|
||||
|
||||
First, you need to fork the repository you want to contribute to. This could be the main `fleetbase/fleetbase` repository or one of the extension repositories. After forking, clone it to your local machine.
|
||||
|
||||
### Step 2: Create or Update a Language File
|
||||
|
||||
Navigate to the appropriate translations directory (`./console/translations/` or `./translations/`).
|
||||
|
||||
- **To add a new language**: Copy the `en-us.yaml` file and rename it to your target language code (e.g., `es-es.yaml`).
|
||||
- **To update an existing language**: Open the existing language file. You can compare it with `en-us.yaml` to find missing keys or phrases that need updating.
|
||||
|
||||
### Step 3: Translate the Content
|
||||
|
||||
Open the YAML file in a text editor. You will see a structure of nested keys and values.
|
||||
|
||||
```yaml
|
||||
# Example from en-us.yaml
|
||||
common:
|
||||
new: New
|
||||
create: Create
|
||||
delete-selected-count: Delete {count} Selected
|
||||
```
|
||||
|
||||
When translating, you should:
|
||||
|
||||
- **Only translate the values**, not the keys. For example, in `new: New`, you would only translate `New`.
|
||||
- **Keep placeholders intact**. Some phrases contain placeholders like `{count}` or `{resource}`. These should not be translated. They are used by the application to insert dynamic values.
|
||||
|
||||
Here is an example of the French translation for the keys above:
|
||||
|
||||
```yaml
|
||||
# Example from fr-fr.yaml
|
||||
common:
|
||||
new: Nouveau
|
||||
create: Créer
|
||||
delete-selected-count: Supprimer {count} sélectionné(s)
|
||||
```
|
||||
|
||||
### Step 4: Submit a Pull Request
|
||||
|
||||
Once you have finished translating, commit your changes and push them to your forked repository. Then, open a pull request to the original Fleetbase repository.
|
||||
|
||||
- Make sure your pull request has a clear title and description of the changes you made.
|
||||
- If you are translating an extension, you may need to submit a pull request to the extension's repository. If your changes also affect the main console, a separate PR to the `fleetbase/fleetbase` repository might be necessary.
|
||||
|
||||
Your contribution will be reviewed by the Fleetbase team, and once approved, it will be merged into the project.
|
||||
|
||||
## Translation Repositories
|
||||
|
||||
Here is a list of the primary repositories that accept translation contributions:
|
||||
|
||||
| Repository | Translation Path |
|
||||
| ---------------------------------------- | ----------------------------- |
|
||||
| [fleetbase/fleetbase][1] | `./console/translations/` |
|
||||
| [fleetbase/fleetops][2] | `./translations/` |
|
||||
| [fleetbase/storefront][3] | `./translations/` |
|
||||
| [fleetbase/dev-engine][4] | `./translations/` |
|
||||
| [fleetbase/iam-engine][5] | `./translations/` |
|
||||
| [fleetbase/pallet][6] | `./translations/` |
|
||||
| [fleetbase/ledger][7] | `./translations/` |
|
||||
| [fleetbase/registry-bridge][8] | `./translations/` |
|
||||
|
||||
[1]: https://github.com/fleetbase/fleetbase
|
||||
[2]: https://github.com/fleetbase/fleetops
|
||||
[3]: https://github.com/fleetbase/storefront
|
||||
[4]: https://github.com/fleetbase/dev-engine
|
||||
[5]: https://github.com/fleetbase/iam-engine
|
||||
[6]: https://github.com/fleetbase/pallet
|
||||
[7]: https://github.com/fleetbase/ledger
|
||||
[8]: https://github.com/fleetbase/registry-bridge
|
||||
|
||||
Thank you again for your contribution to the Fleetbase community!
|
||||
@@ -40,7 +40,6 @@ class Kernel extends HttpKernel
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'throttle:api',
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.0",
|
||||
"php": ">=8.0 <=8.2.28",
|
||||
"appstract/laravel-opcache": "^4.0",
|
||||
"fleetbase/core-api": "^1.6.23",
|
||||
"fleetbase/fleetops-api": "^0.6.24",
|
||||
"fleetbase/registry-bridge": "^0.1.0",
|
||||
"fleetbase/storefront-api": "^0.4.5",
|
||||
"fleetbase/core-api": "^1.6.29",
|
||||
"fleetbase/fleetops-api": "^0.6.31",
|
||||
"fleetbase/registry-bridge": "^0.1.2",
|
||||
"fleetbase/storefront-api": "^0.4.10",
|
||||
"guzzlehttp/guzzle": "^7.0.1",
|
||||
"laravel/framework": "^10.0",
|
||||
"laravel/octane": "^2.3",
|
||||
|
||||
1147
api/composer.lock
generated
1147
api/composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -51,7 +51,7 @@ return [
|
||||
'channels' => [
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => ['single'],
|
||||
'channels' => ['single', 'stdout'],
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
|
||||
@@ -105,8 +105,8 @@ return [
|
||||
OperationTerminated::class => [
|
||||
FlushOnce::class,
|
||||
FlushTemporaryContainerInstances::class,
|
||||
// DisconnectFromDatabases::class,
|
||||
// CollectGarbage::class,
|
||||
DisconnectFromDatabases::class,
|
||||
CollectGarbage::class,
|
||||
],
|
||||
|
||||
WorkerErrorOccurred::class => [
|
||||
|
||||
20
api/resources/lang/es-mx/auth.php
Normal file
20
api/resources/lang/es-mx/auth.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used during authentication for various
|
||||
| messages that we need to display to the user. You are free to modify
|
||||
| these language lines according to your application's requirements.
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => 'Estas credenciales no coinciden con nuestros registros.',
|
||||
'password' => 'La contraseña proporcionada es incorrecta.',
|
||||
'throttle' => 'Demasiados intentos de inicio de sesión. Por favor, intenta de nuevo en :seconds segundos.',
|
||||
|
||||
];
|
||||
19
api/resources/lang/es-mx/pagination.php
Normal file
19
api/resources/lang/es-mx/pagination.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Pagination Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used by the paginator library to build
|
||||
| the simple pagination links. You are free to change them to anything
|
||||
| you want to customize your views to better match your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'previous' => '« Anterior',
|
||||
'next' => 'Siguiente »',
|
||||
|
||||
];
|
||||
22
api/resources/lang/es-mx/passwords.php
Normal file
22
api/resources/lang/es-mx/passwords.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reset Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are the default lines which match reasons
|
||||
| that are given by the password broker for a password update attempt
|
||||
| has failed, such as for an invalid token or invalid new password.
|
||||
|
|
||||
*/
|
||||
|
||||
'reset' => '¡Tu contraseña ha sido restablecida!',
|
||||
'sent' => '¡Te hemos enviado por correo el enlace para restablecer tu contraseña!',
|
||||
'throttled' => 'Por favor espera antes de volver a intentar.',
|
||||
'token' => 'Este token de restablecimiento de contraseña es inválido.',
|
||||
'user' => "No podemos encontrar un usuario con esa dirección de correo electrónico.",
|
||||
|
||||
];
|
||||
163
api/resources/lang/es-mx/validation.php
Normal file
163
api/resources/lang/es-mx/validation.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines contain the default error messages used by
|
||||
| the validator class. Some of these rules have multiple versions such
|
||||
| as the size rules. Feel free to tweak each of these messages here.
|
||||
|
|
||||
*/
|
||||
|
||||
'accepted' => 'El campo :attribute debe ser aceptado.',
|
||||
'accepted_if' => 'El campo :attribute debe ser aceptado cuando :other sea :value.',
|
||||
'active_url' => 'El campo :attribute no es una URL válida.',
|
||||
'after' => 'El campo :attribute debe ser una fecha posterior a :date.',
|
||||
'after_or_equal' => 'El campo :attribute debe ser una fecha posterior o igual a :date.',
|
||||
'alpha' => 'El campo :attribute solo debe contener letras.',
|
||||
'alpha_dash' => 'El campo :attribute solo debe contener letras, números, guiones y guiones bajos.',
|
||||
'alpha_num' => 'El campo :attribute solo debe contener letras y números.',
|
||||
'array' => 'El campo :attribute debe ser un arreglo.',
|
||||
'before' => 'El campo :attribute debe ser una fecha anterior a :date.',
|
||||
'before_or_equal' => 'El campo :attribute debe ser una fecha anterior o igual a :date.',
|
||||
'between' => [
|
||||
'numeric' => 'El campo :attribute debe estar entre :min y :max.',
|
||||
'file' => 'El campo :attribute debe estar entre :min y :max kilobytes.',
|
||||
'string' => 'El campo :attribute debe tener entre :min y :max caracteres.',
|
||||
'array' => 'El campo :attribute debe tener entre :min y :max elementos.',
|
||||
],
|
||||
'boolean' => 'El campo :attribute debe ser verdadero o falso.',
|
||||
'confirmed' => 'La confirmación de :attribute no coincide.',
|
||||
'current_password' => 'La contraseña es incorrecta.',
|
||||
'date' => 'El campo :attribute no es una fecha válida.',
|
||||
'date_equals' => 'El campo :attribute debe ser una fecha igual a :date.',
|
||||
'date_format' => 'El campo :attribute no coincide con el formato :format.',
|
||||
'declined' => 'El campo :attribute debe ser rechazado.',
|
||||
'declined_if' => 'El campo :attribute debe ser rechazado cuando :other sea :value.',
|
||||
'different' => 'El campo :attribute y :other deben ser diferentes.',
|
||||
'digits' => 'El campo :attribute debe tener :digits dígitos.',
|
||||
'digits_between' => 'El campo :attribute debe tener entre :min y :max dígitos.',
|
||||
'dimensions' => 'El campo :attribute tiene dimensiones de imagen inválidas.',
|
||||
'distinct' => 'El campo :attribute tiene un valor duplicado.',
|
||||
'email' => 'El campo :attribute debe ser una dirección de correo electrónico válida.',
|
||||
'ends_with' => 'El campo :attribute debe terminar con uno de los siguientes: :values.',
|
||||
'enum' => 'El :attribute seleccionado es inválido.',
|
||||
'exists' => 'El :attribute seleccionado es inválido.',
|
||||
'file' => 'El campo :attribute debe ser un archivo.',
|
||||
'filled' => 'El campo :attribute debe tener un valor.',
|
||||
'gt' => [
|
||||
'numeric' => 'El campo :attribute debe ser mayor que :value.',
|
||||
'file' => 'El campo :attribute debe ser mayor que :value kilobytes.',
|
||||
'string' => 'El campo :attribute debe ser mayor que :value caracteres.',
|
||||
'array' => 'El campo :attribute debe tener más de :value elementos.',
|
||||
],
|
||||
'gte' => [
|
||||
'numeric' => 'El campo :attribute debe ser mayor o igual a :value.',
|
||||
'file' => 'El campo :attribute debe ser mayor o igual a :value kilobytes.',
|
||||
'string' => 'El campo :attribute debe ser mayor o igual a :value caracteres.',
|
||||
'array' => 'El campo :attribute debe tener :value elementos o más.',
|
||||
],
|
||||
'image' => 'El campo :attribute debe ser una imagen.',
|
||||
'in' => 'El :attribute seleccionado es inválido.',
|
||||
'in_array' => 'El campo :attribute no existe en :other.',
|
||||
'integer' => 'El campo :attribute debe ser un número entero.',
|
||||
'ip' => 'El campo :attribute debe ser una dirección IP válida.',
|
||||
'ipv4' => 'El campo :attribute debe ser una dirección IPv4 válida.',
|
||||
'ipv6' => 'El campo :attribute debe ser una dirección IPv6 válida.',
|
||||
'json' => 'El campo :attribute debe ser una cadena JSON válida.',
|
||||
'lt' => [
|
||||
'numeric' => 'El campo :attribute debe ser menor que :value.',
|
||||
'file' => 'El campo :attribute debe ser menor que :value kilobytes.',
|
||||
'string' => 'El campo :attribute debe ser menor que :value caracteres.',
|
||||
'array' => 'El campo :attribute debe tener menos de :value elementos.',
|
||||
],
|
||||
'lte' => [
|
||||
'numeric' => 'El campo :attribute debe ser menor o igual a :value.',
|
||||
'file' => 'El campo :attribute debe ser menor o igual a :value kilobytes.',
|
||||
'string' => 'El campo :attribute debe ser menor o igual a :value caracteres.',
|
||||
'array' => 'El campo :attribute no debe tener más de :value elementos.',
|
||||
],
|
||||
'mac_address' => 'El campo :attribute debe ser una dirección MAC válida.',
|
||||
'max' => [
|
||||
'numeric' => 'El campo :attribute no debe ser mayor que :max.',
|
||||
'file' => 'El campo :attribute no debe ser mayor que :max kilobytes.',
|
||||
'string' => 'El campo :attribute no debe ser mayor que :max caracteres.',
|
||||
'array' => 'El campo :attribute no debe tener más de :max elementos.',
|
||||
],
|
||||
'mimes' => 'El campo :attribute debe ser un archivo de tipo: :values.',
|
||||
'mimetypes' => 'El campo :attribute debe ser un archivo de tipo: :values.',
|
||||
'min' => [
|
||||
'numeric' => 'El campo :attribute debe ser al menos :min.',
|
||||
'file' => 'El campo :attribute debe ser al menos :min kilobytes.',
|
||||
'string' => 'El campo :attribute debe tener al menos :min caracteres.',
|
||||
'array' => 'El campo :attribute debe tener al menos :min elementos.',
|
||||
],
|
||||
'multiple_of' => 'El campo :attribute debe ser un múltiplo de :value.',
|
||||
'not_in' => 'El :attribute seleccionado es inválido.',
|
||||
'not_regex' => 'El formato del campo :attribute es inválido.',
|
||||
'numeric' => 'El campo :attribute debe ser un número.',
|
||||
'password' => 'La contraseña es incorrecta.',
|
||||
'present' => 'El campo :attribute debe estar presente.',
|
||||
'prohibited' => 'El campo :attribute está prohibido.',
|
||||
'prohibited_if' => 'El campo :attribute está prohibido cuando :other sea :value.',
|
||||
'prohibited_unless' => 'El campo :attribute está prohibido a menos que :other esté en :values.',
|
||||
'prohibits' => 'El campo :attribute prohíbe que :other esté presente.',
|
||||
'regex' => 'El formato del campo :attribute es inválido.',
|
||||
'required' => 'El campo :attribute es obligatorio.',
|
||||
'required_array_keys' => 'El campo :attribute debe contener entradas para: :values.',
|
||||
'required_if' => 'El campo :attribute es obligatorio cuando :other sea :value.',
|
||||
'required_unless' => 'El campo :attribute es obligatorio a menos que :other esté en :values.',
|
||||
'required_with' => 'El campo :attribute es obligatorio cuando :values está presente.',
|
||||
'required_with_all' => 'El campo :attribute es obligatorio cuando :values están presentes.',
|
||||
'required_without' => 'El campo :attribute es obligatorio cuando :values no está presente.',
|
||||
'required_without_all' => 'El campo :attribute es obligatorio cuando ninguno de :values están presentes.',
|
||||
'same' => 'El campo :attribute y :other deben coincidir.',
|
||||
'size' => [
|
||||
'numeric' => 'El campo :attribute debe ser :size.',
|
||||
'file' => 'El campo :attribute debe ser :size kilobytes.',
|
||||
'string' => 'El campo :attribute debe tener :size caracteres.',
|
||||
'array' => 'El campo :attribute debe contener :size elementos.',
|
||||
],
|
||||
'starts_with' => 'El campo :attribute debe comenzar con uno de los siguientes: :values.',
|
||||
'string' => 'El campo :attribute debe ser una cadena de texto.',
|
||||
'timezone' => 'El campo :attribute debe ser una zona horaria válida.',
|
||||
'unique' => 'El campo :attribute ya ha sido tomado.',
|
||||
'uploaded' => 'El campo :attribute falló al subir.',
|
||||
'url' => 'El campo :attribute debe ser una URL válida.',
|
||||
'uuid' => 'El campo :attribute debe ser un UUID válido.',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify custom validation messages for attributes using the
|
||||
| convention "attribute.rule" to name the lines. This makes it quick to
|
||||
| specify a specific custom language line for a given attribute rule.
|
||||
|
|
||||
*/
|
||||
|
||||
'custom' => [
|
||||
'attribute-name' => [
|
||||
'rule-name' => 'custom-message',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Attributes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used to swap our attribute placeholder
|
||||
| with something more reader friendly such as "E-Mail Address" instead
|
||||
| of "email". This simply helps us make our message more expressive.
|
||||
|
|
||||
*/
|
||||
|
||||
'attributes' => [],
|
||||
|
||||
];
|
||||
15
api/resources/lang/fa/auth.php
Normal file
15
api/resources/lang/fa/auth.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| خطوط زبان احراز هویت
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| خطوط زبان زیر در طول احراز هویت برای پیامهای مختلفی که باید به کاربر نمایش دهیم استفاده میشوند.
|
||||
| شما میتوانید این خطوط زبان را بر اساس نیازهای برنامه خود تغییر دهید.
|
||||
|
|
||||
*/
|
||||
'failed' => 'این اطلاعات ورود با سوابق ما مطابقت ندارد.',
|
||||
'password' => 'رمز عبور ارائهشده نادرست است.',
|
||||
'throttle' => 'تعداد تلاشهای ورود بیش از حد زیاد است. لطفاً پس از :seconds ثانیه دوباره تلاش کنید.',
|
||||
];
|
||||
18
api/resources/lang/fa/pagination.php
Normal file
18
api/resources/lang/fa/pagination.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| خطوط زبان صفحهبندی
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| خطوط زبان زیر توسط کتابخانه صفحهبندی برای ساخت لینکهای صفحهبندی ساده استفاده میشوند.
|
||||
| شما میتوانید این خطوط را به دلخواه تغییر دهید تا با نیازهای برنامه خود سازگار شوند.
|
||||
|
|
||||
*/
|
||||
|
||||
'previous' => '« قبلی',
|
||||
'next' => 'بعدی »',
|
||||
|
||||
];
|
||||
22
api/resources/lang/fa/passwords.php
Normal file
22
api/resources/lang/fa/passwords.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| خطوط زبان بازنشانی رمز عبور
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| خطوط زبان زیر خطوط پیشفرضی هستند که با دلایلی که توسط کارگزار رمز عبور
|
||||
| برای تلاشهای ناموفق بهروزرسانی رمز عبور ارائه میشوند، مطابقت دارند،
|
||||
| مانند توکن نامعتبر یا رمز عبور جدید نامعتبر.
|
||||
|
|
||||
*/
|
||||
|
||||
'reset' => 'رمز عبور شما بازنشانی شد!',
|
||||
'sent' => 'لینک بازنشانی رمز عبور به ایمیل شما ارسال شد!',
|
||||
'throttled' => 'لطفاً قبل از تلاش مجدد صبر کنید.',
|
||||
'token' => 'این توکن بازنشانی رمز عبور نامعتبر است.',
|
||||
'user' => 'کاربری با این آدرس ایمیل یافت نشد.',
|
||||
|
||||
];
|
||||
163
api/resources/lang/fa/validation.php
Normal file
163
api/resources/lang/fa/validation.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| خطوط زبان اعتبارسنجی
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| خطوط زبان زیر شامل پیامهای خطای پیشفرض استفادهشده توسط کلاس اعتبارسنجی هستند.
|
||||
| برخی از این قوانین نسخههای متعددی دارند، مانند قوانین مربوط به اندازه.
|
||||
| شما میتوانید این پیامها را در اینجا به دلخواه تنظیم کنید.
|
||||
|
|
||||
*/
|
||||
|
||||
'accepted' => 'فیلد :attribute باید پذیرفته شود.',
|
||||
'accepted_if' => 'فیلد :attribute باید پذیرفته شود وقتی :other برابر با :value باشد.',
|
||||
'active_url' => 'فیلد :attribute یک URL معتبر نیست.',
|
||||
'after' => 'فیلد :attribute باید تاریخی پس از :date باشد.',
|
||||
'after_or_equal' => 'فیلد :attribute باید تاریخی پس از یا برابر با :date باشد.',
|
||||
'alpha' => 'فیلد :attribute فقط باید شامل حروف باشد.',
|
||||
'alpha_dash' => 'فیلد :attribute فقط باید شامل حروف، اعداد، خط تیره و زیرخط باشد.',
|
||||
'alpha_num' => 'فیلد :attribute فقط باید شامل حروف و اعداد باشد.',
|
||||
'array' => 'فیلد :attribute باید یک آرایه باشد.',
|
||||
'before' => 'فیلد :attribute باید تاریخی قبل از :date باشد.',
|
||||
'before_or_equal' => 'فیلد :attribute باید تاریخی قبل از یا برابر با :date باشد.',
|
||||
'between' => [
|
||||
'numeric' => 'فیلد :attribute باید بین :min و :max باشد.',
|
||||
'file' => 'فیلد :attribute باید بین :min و :max کیلوبایت باشد.',
|
||||
'string' => 'فیلد :attribute باید بین :min و :max کاراکتر باشد.',
|
||||
'array' => 'فیلد :attribute باید بین :min و :max آیتم داشته باشد.',
|
||||
],
|
||||
'boolean' => 'فیلد :attribute باید true یا false باشد.',
|
||||
'confirmed' => 'تأیید فیلد :attribute مطابقت ندارد.',
|
||||
'current_password' => 'رمز عبور نادرست است.',
|
||||
'date' => 'فیلد :attribute یک تاریخ معتبر نیست.',
|
||||
'date_equals' => 'فیلد :attribute باید تاریخی برابر با :date باشد.',
|
||||
'date_format' => 'فیلد :attribute با فرمت :format مطابقت ندارد.',
|
||||
'declined' => 'فیلد :attribute باید رد شود.',
|
||||
'declined_if' => 'فیلد :attribute باید رد شود وقتی :other برابر با :value باشد.',
|
||||
'different' => 'فیلد :attribute و :other باید متفاوت باشند.',
|
||||
'digits' => 'فیلد :attribute باید :digits رقم باشد.',
|
||||
'digits_between' => 'فیلد :attribute باید بین :min و :max رقم باشد.',
|
||||
'dimensions' => 'فیلد :attribute دارای ابعاد تصویر نامعتبر است.',
|
||||
'distinct' => 'فیلد :attribute دارای مقدار تکراری است.',
|
||||
'email' => 'فیلد :attribute باید یک آدرس ایمیل معتبر باشد.',
|
||||
'ends_with' => 'فیلد :attribute باید با یکی از مقادیر زیر پایان یابد: :values.',
|
||||
'enum' => 'مقدار انتخابشده برای :attribute نامعتبر است.',
|
||||
'exists' => 'مقدار انتخابشده برای :attribute نامعتبر است.',
|
||||
'file' => 'فیلد :attribute باید یک فایل باشد.',
|
||||
'filled' => 'فیلد :attribute باید دارای مقدار باشد.',
|
||||
'gt' => [
|
||||
'numeric' => 'فیلد :attribute باید بزرگتر از :value باشد.',
|
||||
'file' => 'فیلد :attribute باید بزرگتر از :value کیلوبایت باشد.',
|
||||
'string' => 'فیلد :attribute باید بیش از :value کاراکتر باشد.',
|
||||
'array' => 'فیلد :attribute باید بیش از :value آیتم داشته باشد.',
|
||||
],
|
||||
'gte' => [
|
||||
'numeric' => 'فیلد :attribute باید بزرگتر یا برابر با :value باشد.',
|
||||
'file' => 'فیلد :attribute باید بزرگتر یا برابر با :value کیلوبایت باشد.',
|
||||
'string' => 'فیلد :attribute باید بیش از یا برابر با :value کاراکتر باشد.',
|
||||
'array' => 'فیلد :attribute باید :value آیتم یا بیشتر داشته باشد.',
|
||||
],
|
||||
'image' => 'فیلد :attribute باید یک تصویر باشد.',
|
||||
'in' => 'مقدار انتخابشده برای :attribute نامعتبر است.',
|
||||
'in_array' => 'فیلد :attribute در :other وجود ندارد.',
|
||||
'integer' => 'فیلد :attribute باید یک عدد صحیح باشد.',
|
||||
'ip' => 'فیلد :attribute باید یک آدرس IP معتبر باشد.',
|
||||
'ipv4' => 'فیلد :attribute باید یک آدرس IPv4 معتبر باشد.',
|
||||
'ipv6' => 'فیلد :attribute باید یک آدرس IPv6 معتبر باشد.',
|
||||
'json' => 'فیلد :attribute باید یک رشته JSON معتبر باشد.',
|
||||
'lt' => [
|
||||
'numeric' => 'فیلد :attribute باید کمتر از :value باشد.',
|
||||
'file' => 'فیلد :attribute باید کمتر از :value کیلوبایت باشد.',
|
||||
'string' => 'فیلد :attribute باید کمتر از :value کاراکتر باشد.',
|
||||
'array' => 'فیلد :attribute باید کمتر از :value آیتم داشته باشد.',
|
||||
],
|
||||
'lte' => [
|
||||
'numeric' => 'فیلد :attribute باید کمتر یا برابر با :value باشد.',
|
||||
'file' => 'فیلد :attribute باید کمتر یا برابر با :value کیلوبایت باشد.',
|
||||
'string' => 'فیلد :attribute باید کمتر یا برابر با :value کاراکتر باشد.',
|
||||
'array' => 'فیلد :attribute نباید بیش از :value آیتم داشته باشد.',
|
||||
],
|
||||
'mac_address' => 'فیلد :attribute باید یک آدرس MAC معتبر باشد.',
|
||||
'max' => [
|
||||
'numeric' => 'فیلد :attribute نباید بزرگتر از :max باشد.',
|
||||
'file' => 'فیلد :attribute نباید بزرگتر از :max کیلوبایت باشد.',
|
||||
'string' => 'فیلد :attribute نباید بیش از :max کاراکتر باشد.',
|
||||
'array' => 'فیلد :attribute نباید بیش از :max آیتم داشته باشد.',
|
||||
],
|
||||
'mimes' => 'فیلد :attribute باید یک فایل از نوع: :values باشد.',
|
||||
'mimetypes' => 'فیلد :attribute باید یک فایل از نوع: :values باشد.',
|
||||
'min' => [
|
||||
'numeric' => 'فیلد :attribute باید حداقل :min باشد.',
|
||||
'file' => 'فیلد :attribute باید حداقل :min کیلوبایت باشد.',
|
||||
'string' => 'فیلد :attribute باید حداقل :min کاراکتر باشد.',
|
||||
'array' => 'فیلد :attribute باید حداقل :min آیتم داشته باشد.',
|
||||
],
|
||||
'multiple_of' => 'فیلد :attribute باید مضربی از :value باشد.',
|
||||
'not_in' => 'مقدار انتخابشده برای :attribute نامعتبر است.',
|
||||
'not_regex' => 'فرمت فیلد :attribute نامعتبر است.',
|
||||
'numeric' => 'فیلد :attribute باید یک عدد باشد.',
|
||||
'password' => 'رمز عبور نادرست است.',
|
||||
'present' => 'فیلد :attribute باید وجود داشته باشد.',
|
||||
'prohibited' => 'فیلد :attribute ممنوع است.',
|
||||
'prohibited_if' => 'فیلد :attribute وقتی :other برابر با :value باشد ممنوع است.',
|
||||
'prohibited_unless' => 'فیلد :attribute ممنوع است مگر اینکه :other در :values باشد.',
|
||||
'prohibits' => 'فیلد :attribute مانع حضور :other میشود.',
|
||||
'regex' => 'فرمت فیلد :attribute نامعتبر است.',
|
||||
'required' => 'فیلد :attribute الزامی است.',
|
||||
'required_array_keys' => 'فیلد :attribute باید شامل ورودیهایی برای: :values باشد.',
|
||||
'required_if' => 'فیلد :attribute وقتی :other برابر با :value باشد الزامی است.',
|
||||
'required_unless' => 'فیلد :attribute الزامی است مگر اینکه :other در :values باشد.',
|
||||
'required_with' => 'فیلد :attribute وقتی :values وجود دارد الزامی است.',
|
||||
'required_with_all' => 'فیلد :attribute وقتی همه :values وجود دارند الزامی است.',
|
||||
'required_without' => 'فیلد :attribute وقتی :values وجود ندارد الزامی است.',
|
||||
'required_without_all' => 'فیلد :attribute وقتی هیچکدام از :values وجود ندارند الزامی است.',
|
||||
'same' => 'فیلد :attribute و :other باید یکسان باشند.',
|
||||
'size' => [
|
||||
'numeric' => 'فیلد :attribute باید :size باشد.',
|
||||
'file' => 'فیلد :attribute باید :size کیلوبایت باشد.',
|
||||
'string' => 'فیلد :attribute باید :size کاراکتر باشد.',
|
||||
'array' => 'فیلد :attribute باید شامل :size آیتم باشد.',
|
||||
],
|
||||
'starts_with' => 'فیلد :attribute باید با یکی از مقادیر زیر شروع شود: :values.',
|
||||
'string' => 'فیلد :attribute باید یک رشته باشد.',
|
||||
'timezone' => 'فیلد :attribute باید یک منطقه زمانی معتبر باشد.',
|
||||
'unique' => 'فیلد :attribute قبلاً استفاده شده است.',
|
||||
'uploaded' => 'فیلد :attribute در آپلود ناموفق بود.',
|
||||
'url' => 'فیلد :attribute باید یک URL معتبر باشد.',
|
||||
'uuid' => 'فیلد :attribute باید یک UUID معتبر باشد.',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| خطوط زبان اعتبارسنجی سفارشی
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| در اینجا میتوانید پیامهای اعتبارسنجی سفارشی برای ویژگیها را با استفاده از
|
||||
| قرارداد "attribute.rule" برای نامگذاری خطوط مشخص کنید. این کار امکان
|
||||
| تعیین سریع یک خط زبان سفارشی برای یک قانون خاص ویژگی را فراهم میکند.
|
||||
|
|
||||
*/
|
||||
|
||||
'custom' => [
|
||||
'attribute-name' => [
|
||||
'rule-name' => 'پیام سفارشی',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| ویژگیهای اعتبارسنجی سفارشی
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| خطوط زبان زیر برای جایگزینی placeholder ویژگیهای ما با چیزی کاربرپسندتر
|
||||
| مانند "آدرس ایمیل" به جای "email" استفاده میشوند. این کار به ما کمک میکند
|
||||
| پیامهایمان را گویاتر کنیم.
|
||||
|
|
||||
*/
|
||||
|
||||
'attributes' => [],
|
||||
|
||||
];
|
||||
4
console/.gitignore
vendored
4
console/.gitignore
vendored
@@ -26,3 +26,7 @@
|
||||
|
||||
# broccoli-debug
|
||||
/DEBUG/
|
||||
|
||||
# Auto-generated extension files
|
||||
/app/extensions/
|
||||
/app/utils/extension-loaders.js
|
||||
|
||||
@@ -2,10 +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 loadExtensions from '@fleetbase/ember-core/utils/load-extensions';
|
||||
import mapEngines from '@fleetbase/ember-core/utils/map-engines';
|
||||
import loadRuntimeConfig from '@fleetbase/console/utils/runtime-config';
|
||||
import applyRouterFix from './utils/router-refresh-patch';
|
||||
import './deprecation-workflow';
|
||||
|
||||
export default class App extends Application {
|
||||
modulePrefix = config.modulePrefix;
|
||||
@@ -13,21 +10,6 @@ export default class App extends Application {
|
||||
Resolver = Resolver;
|
||||
extensions = [];
|
||||
engines = {};
|
||||
|
||||
async ready() {
|
||||
applyRouterFix(this);
|
||||
const extensions = await loadExtensions();
|
||||
|
||||
this.extensions = extensions;
|
||||
this.engines = mapEngines(extensions);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await loadRuntimeConfig();
|
||||
loadInitializers(App, config.modulePrefix);
|
||||
|
||||
let fleetbase = App.create();
|
||||
fleetbase.deferReadiness();
|
||||
fleetbase.boot();
|
||||
});
|
||||
loadInitializers(App, config.modulePrefix);
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="px-4 py-2.5">
|
||||
{{#if this.isLoading}}
|
||||
{{#if this.loadBlogPosts.isRunning}}
|
||||
<Spinner />
|
||||
{{else}}
|
||||
<ul class="space-y-2">
|
||||
|
||||
@@ -1,28 +1,42 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { isArray } from '@ember/array';
|
||||
import { storageFor } from 'ember-local-storage';
|
||||
import { add, isPast } from 'date-fns';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
export default class FleetbaseBlogComponent extends Component {
|
||||
@storageFor('local-cache') localCache;
|
||||
@service fetch;
|
||||
@tracked posts = [];
|
||||
@tracked isLoading = false;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.loadBlogPosts();
|
||||
this.loadBlogPosts.perform();
|
||||
}
|
||||
|
||||
@action loadBlogPosts() {
|
||||
this.isLoading = true;
|
||||
@task *loadBlogPosts() {
|
||||
// Check if cached data and expiration are available
|
||||
const cachedData = this.localCache.get('fleetbase-blog-data');
|
||||
const expiration = this.localCache.get('fleetbase-blog-data-expiration');
|
||||
|
||||
return this.fetch
|
||||
.get('lookup/fleetbase-blog')
|
||||
.then((response) => {
|
||||
this.posts = response;
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
// Check if the cached data is still valid
|
||||
if (cachedData && isArray(cachedData) && expiration && !isPast(new Date(expiration))) {
|
||||
// Use cached data
|
||||
this.posts = cachedData;
|
||||
} else {
|
||||
// Fetch new data
|
||||
try {
|
||||
const data = yield this.fetch.get('lookup/fleetbase-blog');
|
||||
this.posts = isArray(data) ? data : [];
|
||||
if (data) {
|
||||
this.localCache.set('fleetbase-blog-data', data);
|
||||
this.localCache.set('fleetbase-blog-data-expiration', add(new Date(), { hours: 6 }));
|
||||
}
|
||||
} catch (err) {
|
||||
debug('Failed to load blog: ' + err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export default class GithubCardComponent extends Component {
|
||||
this.data = cachedData;
|
||||
} else {
|
||||
// Fetch new data
|
||||
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase');
|
||||
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase', { cache: 'default' });
|
||||
if (response.ok) {
|
||||
this.data = yield response.json();
|
||||
this.localCache.set('fleetbase-github-data', this.data);
|
||||
@@ -72,7 +72,7 @@ export default class GithubCardComponent extends Component {
|
||||
this.tags = cachedTags;
|
||||
} else {
|
||||
// Fetch new tags
|
||||
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase/tags');
|
||||
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase/tags', { cache: 'default' });
|
||||
if (response.ok) {
|
||||
this.tags = yield response.json();
|
||||
this.localCache.set('fleetbase-github-tags', this.tags);
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -2,10 +2,6 @@ import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ConsoleAccountController extends Controller {
|
||||
/**
|
||||
* Inject the `universe` service.
|
||||
*
|
||||
* @memberof ConsoleAdminController
|
||||
*/
|
||||
@service('universe/menu-service') menuService;
|
||||
@service universe;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -2,10 +2,6 @@ import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ConsoleAdminController extends Controller {
|
||||
/**
|
||||
* Inject the `universe` service.
|
||||
*
|
||||
* @memberof ConsoleAdminController
|
||||
*/
|
||||
@service('universe/menu-service') menuService;
|
||||
@service universe;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,6 @@ import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ConsoleSettingsController extends Controller {
|
||||
/**
|
||||
* INject the `universe` service
|
||||
*
|
||||
* @memberof ConsoleSettingsController
|
||||
*/
|
||||
@service('universe/menu-service') menuService;
|
||||
@service universe;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
console/app/controllers/virtual.js
Normal file
7
console/app/controllers/virtual.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class VirtualController extends Controller {
|
||||
@tracked view;
|
||||
queryParams = ['view'];
|
||||
}
|
||||
9
console/app/deprecation-workflow.js
Normal file
9
console/app/deprecation-workflow.js
Normal 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' },
|
||||
],
|
||||
});
|
||||
@@ -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">
|
||||
|
||||
@@ -38,4 +38,7 @@ export function initialize(application) {
|
||||
})();
|
||||
}
|
||||
|
||||
export default { initialize };
|
||||
export default {
|
||||
name: 'load-intl-polyfills',
|
||||
initialize,
|
||||
};
|
||||
|
||||
41
console/app/initializers/load-runtime-config.js
Normal file
41
console/app/initializers/load-runtime-config.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import loadRuntimeConfig from '@fleetbase/console/utils/runtime-config';
|
||||
import { debug } from '@ember/debug';
|
||||
|
||||
/**
|
||||
* Load Runtime Config Initializer
|
||||
*
|
||||
* Loads runtime configuration from fleetbase.config.json before the application boots.
|
||||
* This must run first to ensure all config is available for other initializers.
|
||||
*
|
||||
* Uses `before` to ensure it runs before any other initializers.
|
||||
*
|
||||
* @export
|
||||
* @param {Application} application
|
||||
*/
|
||||
export function initialize(application) {
|
||||
const startTime = performance.now();
|
||||
debug('[Runtime Config] Loading runtime configuration...');
|
||||
|
||||
// Defer readiness until config is loaded
|
||||
application.deferReadiness();
|
||||
(async () => {
|
||||
try {
|
||||
await loadRuntimeConfig();
|
||||
const endTime = performance.now();
|
||||
debug(`[Runtime Config] Runtime config loaded in ${(endTime - startTime).toFixed(2)}ms`);
|
||||
application.advanceReadiness();
|
||||
} catch (error) {
|
||||
console.error('[Runtime Config] Failed to load runtime config:', error);
|
||||
// Still advance readiness to prevent hanging
|
||||
application.advanceReadiness();
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'load-runtime-config',
|
||||
initialize,
|
||||
// Run after intl polyfills are loaded, before socketcluster
|
||||
after: 'load-intl-polyfills',
|
||||
before: 'load-socketcluster-client',
|
||||
};
|
||||
38
console/app/instance-initializers/apply-router-fix.js
Normal file
38
console/app/instance-initializers/apply-router-fix.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import applyRouterFix from '@fleetbase/console/utils/router-refresh-patch';
|
||||
import { debug } from '@ember/debug';
|
||||
|
||||
/**
|
||||
* Apply Router Fix Instance Initializer
|
||||
*
|
||||
* Applies the Fleetbase router refresh bug fix patch.
|
||||
* This patches the Ember router to handle dynamic segments correctly
|
||||
* when refreshing routes with query parameters.
|
||||
*
|
||||
* Runs as an instance-initializer because it needs access to the
|
||||
* application instance and router service.
|
||||
*
|
||||
* Bug: https://github.com/emberjs/ember.js/issues/19260
|
||||
*
|
||||
* @export
|
||||
* @param {ApplicationInstance} appInstance
|
||||
*/
|
||||
export function initialize(appInstance) {
|
||||
const startTime = performance.now();
|
||||
debug('[Initializing Router Patch] Applying router refresh bug fix...');
|
||||
|
||||
try {
|
||||
applyRouterFix(appInstance);
|
||||
|
||||
const endTime = performance.now();
|
||||
debug(`[Initializing Router Patch] Router fix applied in ${(endTime - startTime).toFixed(2)}ms`);
|
||||
} catch (error) {
|
||||
console.error('[Initializing Router Patch] Failed to apply router fix:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'apply-router-fix',
|
||||
initialize,
|
||||
// Run before extension loading to ensure router is patched early
|
||||
before: 'load-extensions',
|
||||
};
|
||||
20
console/app/instance-initializers/initialize-registries.js
Normal file
20
console/app/instance-initializers/initialize-registries.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { debug } from '@ember/debug';
|
||||
|
||||
/**
|
||||
* Create console-specific registries
|
||||
* Runs after extensions are loaded
|
||||
*/
|
||||
export function initialize(appInstance) {
|
||||
const registryService = appInstance.lookup('service:universe/registry-service');
|
||||
|
||||
debug('[Initializing Registries] Creating console registries...');
|
||||
|
||||
// Create console-specific registries
|
||||
registryService.createRegistries(['@fleetbase/console', 'auth:login']);
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'initialize-registries',
|
||||
after: 'load-extensions',
|
||||
initialize,
|
||||
};
|
||||
@@ -1,36 +1,47 @@
|
||||
import { Widget } from '@fleetbase/ember-core/contracts';
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||
import { debug } from '@ember/debug';
|
||||
|
||||
export function initialize(application) {
|
||||
const universe = application.lookup('service:universe');
|
||||
const defaultWidgets = [
|
||||
{
|
||||
widgetId: 'fleetbase-blog',
|
||||
/**
|
||||
* Register dashboard and widgets for FleetbaseConsole
|
||||
* Runs after extensions are loaded
|
||||
*/
|
||||
export function initialize(appInstance) {
|
||||
const widgetService = appInstance.lookup('service:universe/widget-service');
|
||||
|
||||
debug('[Initializing Widgets] Registering console dashboard and widgets...');
|
||||
|
||||
// Register the console dashboard
|
||||
widgetService.registerDashboard('dashboard');
|
||||
|
||||
// Create widget definitions
|
||||
const widgets = [
|
||||
new Widget({
|
||||
id: 'fleetbase-blog',
|
||||
name: 'Fleetbase Blog',
|
||||
description: 'Lists latest news and events from the Fleetbase official team.',
|
||||
icon: 'newspaper',
|
||||
component: 'fleetbase-blog',
|
||||
grid_options: { w: 8, h: 9, minW: 8, minH: 9 },
|
||||
options: {
|
||||
title: 'Fleetbase Blog',
|
||||
},
|
||||
},
|
||||
{
|
||||
widgetId: 'fleetbase-github-card',
|
||||
default: true,
|
||||
}),
|
||||
new Widget({
|
||||
id: 'fleetbase-github-card',
|
||||
name: 'Github Card',
|
||||
description: 'Displays current Github stats from the official Fleetbase repo.',
|
||||
icon: faGithub,
|
||||
component: 'github-card',
|
||||
grid_options: { w: 4, h: 8, minW: 4, minH: 8 },
|
||||
options: {
|
||||
title: 'Github Card',
|
||||
},
|
||||
},
|
||||
default: true,
|
||||
}),
|
||||
];
|
||||
|
||||
universe.registerDefaultDashboardWidgets(defaultWidgets);
|
||||
universe.registerDashboardWidgets(defaultWidgets);
|
||||
// Register widgets
|
||||
widgetService.registerWidgets('dashboard', widgets);
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'initialize-widgets',
|
||||
after: 'load-extensions',
|
||||
initialize,
|
||||
};
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
export function initialize(application) {
|
||||
const universe = application.lookup('service:universe');
|
||||
if (universe) {
|
||||
universe.createRegistries(['@fleetbase/console', 'auth:login']);
|
||||
universe.bootEngines(application);
|
||||
/**
|
||||
* Load extensions from the API using ExtensionManager
|
||||
* This must run before other initializers that depend on extensions
|
||||
*/
|
||||
export async function initialize(appInstance) {
|
||||
const application = appInstance.application;
|
||||
const extensionManager = appInstance.lookup('service:universe/extension-manager');
|
||||
|
||||
try {
|
||||
await extensionManager.loadExtensions(application);
|
||||
} catch (error) {
|
||||
console.error('[load-extensions] Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'load-extensions',
|
||||
initialize,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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) {
|
||||
universeService.setApplicationInstance(appInstance);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
initialize
|
||||
};
|
||||
16
console/app/instance-initializers/setup-extensions.js
Normal file
16
console/app/instance-initializers/setup-extensions.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Setup extensions by loading and executing their extension.js files
|
||||
* Runs after extensions are loaded from API
|
||||
*/
|
||||
export async function initialize(appInstance) {
|
||||
const universe = appInstance.lookup('service:universe');
|
||||
const extensionManager = appInstance.lookup('service:universe/extension-manager');
|
||||
|
||||
await extensionManager.setupExtensions(appInstance, universe);
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'setup-extensions',
|
||||
after: ['load-extensions', 'initialize-registries', 'initialize-widgets'],
|
||||
initialize,
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
18
console/app/models/schedule-availability.js
Normal file
18
console/app/models/schedule-availability.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
|
||||
export default class ScheduleAvailabilityModel extends Model {
|
||||
@attr('string') subject_uuid;
|
||||
@attr('string') subject_type;
|
||||
@attr('date') start_at;
|
||||
@attr('date') end_at;
|
||||
@attr('boolean', { defaultValue: true }) is_available;
|
||||
@attr('number') preference_level;
|
||||
@attr('string') rrule;
|
||||
@attr('string') reason;
|
||||
@attr('string') notes;
|
||||
@attr('object') meta;
|
||||
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
@attr('date') deleted_at;
|
||||
}
|
||||
23
console/app/models/schedule-constraint.js
Normal file
23
console/app/models/schedule-constraint.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import Model, { attr, belongsTo } from '@ember-data/model';
|
||||
|
||||
export default class ScheduleConstraintModel extends Model {
|
||||
@attr('string') company_uuid;
|
||||
@attr('string') subject_uuid;
|
||||
@attr('string') subject_type;
|
||||
@attr('string') name;
|
||||
@attr('string') description;
|
||||
@attr('string') type;
|
||||
@attr('string') category;
|
||||
@attr('string') constraint_key;
|
||||
@attr('string') constraint_value;
|
||||
@attr('string') jurisdiction;
|
||||
@attr('number', { defaultValue: 0 }) priority;
|
||||
@attr('boolean', { defaultValue: true }) is_active;
|
||||
@attr('object') meta;
|
||||
|
||||
@belongsTo('company') company;
|
||||
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
@attr('date') deleted_at;
|
||||
}
|
||||
23
console/app/models/schedule-item.js
Normal file
23
console/app/models/schedule-item.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import Model, { attr, belongsTo } from '@ember-data/model';
|
||||
|
||||
export default class ScheduleItemModel extends Model {
|
||||
@attr('string') public_id;
|
||||
@attr('string') schedule_uuid;
|
||||
@attr('string') assignee_uuid;
|
||||
@attr('string') assignee_type;
|
||||
@attr('string') resource_uuid;
|
||||
@attr('string') resource_type;
|
||||
@attr('date') start_at;
|
||||
@attr('date') end_at;
|
||||
@attr('number') duration;
|
||||
@attr('date') break_start_at;
|
||||
@attr('date') break_end_at;
|
||||
@attr('string', { defaultValue: 'pending' }) status;
|
||||
@attr('object') meta;
|
||||
|
||||
@belongsTo('schedule') schedule;
|
||||
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
@attr('date') deleted_at;
|
||||
}
|
||||
22
console/app/models/schedule-template.js
Normal file
22
console/app/models/schedule-template.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import Model, { attr, belongsTo } from '@ember-data/model';
|
||||
|
||||
export default class ScheduleTemplateModel extends Model {
|
||||
@attr('string') public_id;
|
||||
@attr('string') company_uuid;
|
||||
@attr('string') subject_uuid;
|
||||
@attr('string') subject_type;
|
||||
@attr('string') name;
|
||||
@attr('string') description;
|
||||
@attr('string') start_time;
|
||||
@attr('string') end_time;
|
||||
@attr('number') duration;
|
||||
@attr('number') break_duration;
|
||||
@attr('string') rrule;
|
||||
@attr('object') meta;
|
||||
|
||||
@belongsTo('company') company;
|
||||
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
@attr('date') deleted_at;
|
||||
}
|
||||
27
console/app/models/schedule.js
Normal file
27
console/app/models/schedule.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import Model, { attr, hasMany, belongsTo } from '@ember-data/model';
|
||||
|
||||
export default class ScheduleModel extends Model {
|
||||
/** @ids */
|
||||
@attr('string') public_id;
|
||||
@attr('string') company_uuid;
|
||||
@attr('string') subject_uuid;
|
||||
@attr('string') subject_type;
|
||||
|
||||
/** @attributes */
|
||||
@attr('string') name;
|
||||
@attr('string') description;
|
||||
@attr('date') start_date;
|
||||
@attr('date') end_date;
|
||||
@attr('string') timezone;
|
||||
@attr('string', { defaultValue: 'draft' }) status;
|
||||
@attr('object') meta;
|
||||
|
||||
/** @relationships */
|
||||
@hasMany('schedule-item') items;
|
||||
@belongsTo('company') company;
|
||||
|
||||
/** @dates */
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
@attr('date') deleted_at;
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import pathToRoute from '@fleetbase/ember-core/utils/path-to-route';
|
||||
import removeBootLoader from '../utils/remove-boot-loader';
|
||||
|
||||
export default class ApplicationRoute extends Route {
|
||||
@service('universe/hook-service') hookService;
|
||||
@service('universe/extension-manager') extensionManager;
|
||||
@service session;
|
||||
@service theme;
|
||||
@service fetch;
|
||||
@@ -15,7 +17,6 @@ export default class ApplicationRoute extends Route {
|
||||
@service intl;
|
||||
@service currentUser;
|
||||
@service router;
|
||||
@service universe;
|
||||
@tracked defaultTheme;
|
||||
|
||||
/**
|
||||
@@ -24,7 +25,7 @@ export default class ApplicationRoute extends Route {
|
||||
* @memberof ApplicationRoute
|
||||
*/
|
||||
@action willTransition(transition) {
|
||||
this.universe.callHooks('application:will-transition', this.session, this.router, transition);
|
||||
this.hookService.execute('application:will-transition', this.session, this.router, transition);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,7 +46,7 @@ export default class ApplicationRoute extends Route {
|
||||
* @memberof ApplicationRoute
|
||||
*/
|
||||
@action loading(transition) {
|
||||
this.universe.callHooks('application:loading', this.session, this.router, transition);
|
||||
this.hookService.execute('application:loading', this.session, this.router, transition);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,9 +80,9 @@ export default class ApplicationRoute extends Route {
|
||||
*/
|
||||
async beforeModel(transition) {
|
||||
await this.session.setup();
|
||||
await this.universe.booting();
|
||||
await this.extensionManager.waitForBoot();
|
||||
|
||||
this.universe.callHooks('application:before-model', this.session, this.router, transition);
|
||||
this.hookService.execute('application:before-model', this.session, this.router, transition);
|
||||
|
||||
const shift = this.urlSearchParams.get('shift');
|
||||
if (this.session.isAuthenticated && shift) {
|
||||
@@ -95,9 +96,7 @@ export default class ApplicationRoute extends Route {
|
||||
* @memberof ApplicationRoute
|
||||
*/
|
||||
afterModel() {
|
||||
if (!this.session.isAuthenticated) {
|
||||
removeBootLoader();
|
||||
}
|
||||
if (!this.session.isAuthenticated) removeBootLoader();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,11 +121,11 @@ export default class ApplicationRoute extends Route {
|
||||
* Initializes the application's locale settings based on the current user's preferences.
|
||||
*
|
||||
* This method retrieves the user's preferred locale using the `getOption` method from the `currentUser` service.
|
||||
* If no locale is set by the user, it defaults to `'en-us'`. It then sets the application's locale by calling
|
||||
* If no locale is set by the user, it defaults to `'en-US'`. It then sets the application's locale by calling
|
||||
* the `setLocale` method of the `intl` service with the retrieved locale.
|
||||
*/
|
||||
initializeLocale() {
|
||||
const locale = this.currentUser.getOption('locale', 'en-us');
|
||||
const locale = this.currentUser.getOption('locale', 'en-US');
|
||||
this.intl.setLocale([locale]);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import removeBootLoader from '../utils/remove-boot-loader';
|
||||
import '@fleetbase/leaflet-routing-machine';
|
||||
|
||||
export default class ConsoleRoute extends Route {
|
||||
@service('universe/hook-service') hookService;
|
||||
@service store;
|
||||
@service session;
|
||||
@service universe;
|
||||
@service router;
|
||||
@service currentUser;
|
||||
@service intl;
|
||||
@@ -22,7 +22,7 @@ export default class ConsoleRoute extends Route {
|
||||
async beforeModel(transition) {
|
||||
await this.session.requireAuthentication(transition, 'auth.login');
|
||||
|
||||
this.universe.callHooks('console:before-model', this.session, this.router, transition);
|
||||
this.hookService.execute('console:before-model', this.session, this.router, transition);
|
||||
|
||||
if (this.session.isAuthenticated) {
|
||||
return this.session.promiseCurrentUser(transition);
|
||||
@@ -37,7 +37,7 @@ export default class ConsoleRoute extends Route {
|
||||
* @memberof ConsoleRoute
|
||||
*/
|
||||
async afterModel(model, transition) {
|
||||
this.universe.callHooks('console:after-model', this.session, this.router, model, transition);
|
||||
this.hookService.execute('console:after-model', this.session, this.router, model, transition);
|
||||
removeBootLoader();
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export default class ConsoleRoute extends Route {
|
||||
* @memberof ConsoleRoute
|
||||
*/
|
||||
@action didTransition() {
|
||||
this.universe.callHooks('console:did-transition', this.session, this.router);
|
||||
this.hookService.execute('console:did-transition', this.session, this.router);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ConsoleAccountVirtualRoute extends Route {
|
||||
@service('universe/menu-service') menuService;
|
||||
@service universe;
|
||||
|
||||
queryParams = {
|
||||
@@ -12,6 +13,6 @@ export default class ConsoleAccountVirtualRoute extends Route {
|
||||
|
||||
model({ slug }, transition) {
|
||||
const view = this.universe.getViewFromTransition(transition);
|
||||
return this.universe.lookupMenuItemFromRegistry('console:account', slug, view);
|
||||
return this.menuService.lookupMenuItem('console:account', slug, view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ConsoleAdminVirtualRoute extends Route {
|
||||
@service('universe/menu-service') menuService;
|
||||
@service universe;
|
||||
|
||||
queryParams = {
|
||||
@@ -12,6 +13,6 @@ export default class ConsoleAdminVirtualRoute extends Route {
|
||||
|
||||
model({ slug }, transition) {
|
||||
const view = this.universe.getViewFromTransition(transition);
|
||||
return this.universe.lookupMenuItemFromRegistry('console:admin', slug, view);
|
||||
return this.menuService.lookupMenuItem('console:admin', slug, view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ConsoleSettingsVirtualRoute extends Route {
|
||||
@service('universe/menu-service') menuService;
|
||||
@service universe;
|
||||
|
||||
queryParams = {
|
||||
@@ -12,6 +13,6 @@ export default class ConsoleSettingsVirtualRoute extends Route {
|
||||
|
||||
model({ slug }, transition) {
|
||||
const view = this.universe.getViewFromTransition(transition);
|
||||
return this.universe.lookupMenuItemFromRegistry('console:settings', slug, view);
|
||||
return this.menuService.lookupMenuItem('console:settings', slug, view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ConsoleVirtualRoute extends Route {
|
||||
@service('universe/menu-service') menuService;
|
||||
@service universe;
|
||||
|
||||
queryParams = {
|
||||
@@ -12,6 +13,6 @@ export default class ConsoleVirtualRoute extends Route {
|
||||
|
||||
model({ slug }, transition) {
|
||||
const view = this.universe.getViewFromTransition(transition);
|
||||
return this.universe.lookupMenuItemFromRegistry('console', slug, view);
|
||||
return this.menuService.lookupMenuItem('console', slug, view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class VirtualRoute extends Route {
|
||||
@service('universe/menu-service') menuService;
|
||||
@service universe;
|
||||
|
||||
queryParams = {
|
||||
@@ -12,6 +13,6 @@ export default class VirtualRoute extends Route {
|
||||
|
||||
model({ slug }, transition) {
|
||||
const view = this.universe.getViewFromTransition(transition);
|
||||
return this.universe.lookupMenuItemFromRegistry('auth:login', slug, view);
|
||||
return this.menuService.lookupMenuItem('auth:login', slug, view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -7,17 +7,31 @@ 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 = {}) {
|
||||
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);
|
||||
|
||||
// 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 +39,43 @@ 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);
|
||||
|
||||
// Support both string and function for next property
|
||||
let nextId;
|
||||
if (typeof leaving.next === 'function') {
|
||||
nextId = leaving.next(this.onboardingContext);
|
||||
@@ -53,8 +83,20 @@ 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);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -68,4 +110,31 @@ export default class OnboardingOrchestratorService extends Service {
|
||||
this.history = this.history.slice(0, -1);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -107,8 +107,12 @@ export default class UserVerificationService extends Service {
|
||||
}
|
||||
|
||||
#wait(timeout = 75000) {
|
||||
return later(this, () => {
|
||||
this.waiting = true;
|
||||
}, timeout);
|
||||
return later(
|
||||
this,
|
||||
() => {
|
||||
this.waiting = true;
|
||||
},
|
||||
timeout
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
<Layout::Sidebar::Item @route="console.account.index" @icon="user">Profile</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.account.auth" @icon="key">Auth</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.account.organizations" @icon="building">Organizations</Layout::Sidebar::Item>
|
||||
{{#each this.universe.accountMenuItems as |menuItem|}}
|
||||
{{#each this.menuService.accountMenuItems as |menuItem|}}
|
||||
<Layout::Sidebar::Item @onClick={{fn this.universe.transitionMenuItem "console.account.virtual" menuItem}} @item={{menuItem}} @icon={{menuItem.icon}}>{{menuItem.title}}</Layout::Sidebar::Item>
|
||||
{{/each}}
|
||||
</Layout::Sidebar::Panel>
|
||||
{{#each this.universe.accountMenuPanels as |menuPanel|}}
|
||||
{{#each this.menuService.accountMenuPanels as |menuPanel|}}
|
||||
<Layout::Sidebar::Panel @open={{menuPanel.open}} @title={{menuPanel.title}}>
|
||||
{{#each menuPanel.items as |menuItem|}}
|
||||
<Layout::Sidebar::Item @onClick={{fn this.universe.transitionMenuItem "console.account.virtual" menuItem}} @item={{menuItem}} @icon={{menuItem.icon}}>{{menuItem.title}}</Layout::Sidebar::Item>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<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-">
|
||||
{{component @model.component params=@model.componentParams}}
|
||||
<LazyEngineComponent @component={{@model.component}} @params={{@model.componentParams}} />
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
|
||||
@@ -6,14 +6,16 @@
|
||||
<Layout::Sidebar::Item @route="console.admin.branding" @icon="palette">{{t "console.admin.menu.branding"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.two-fa-settings" @icon="shield-halved">{{t "console.admin.menu.2fa-config"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.schedule-monitor" @icon="calendar-check">{{t "console.admin.schedule-monitor.schedule-monitor"}}</Layout::Sidebar::Item>
|
||||
{{#each this.universe.adminMenuItems as |menuItem|}}
|
||||
|
||||
{{#each this.menuService.adminMenuItems as |menuItem|}}
|
||||
<Layout::Sidebar::Item
|
||||
@onClick={{fn this.universe.transitionMenuItem "console.admin.virtual" menuItem}}
|
||||
@item={{menuItem}}
|
||||
@icon={{menuItem.icon}}
|
||||
>{{menuItem.title}}</Layout::Sidebar::Item>
|
||||
{{/each}}
|
||||
{{#each this.universe.adminMenuPanels as |menuPanel|}}
|
||||
|
||||
{{#each this.menuService.adminMenuPanels as |menuPanel|}}
|
||||
<Layout::Sidebar::Panel @open={{menuPanel.open}} @title={{menuPanel.title}}>
|
||||
{{#each menuPanel.items as |menuItem|}}
|
||||
<Layout::Sidebar::Item
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<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-">
|
||||
{{component @model.component params=@model.componentParams}}
|
||||
<LazyEngineComponent @component={{@model.component}} @params={{@model.componentParams}} />
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<Layout::Sidebar::Item @route="console.settings.index" @icon="cog">{{t "common.organization"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.settings.two-fa" @icon="shield-halved">{{t "common.two-factor"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.settings.notifications" @icon="bell">{{t "common.notifications"}}</Layout::Sidebar::Item>
|
||||
{{#each this.universe.settingsMenuItems as |menuItem|}}
|
||||
{{#each this.menuService.settingsMenuItems as |menuItem|}}
|
||||
<Layout::Sidebar::Item
|
||||
@onClick={{fn this.universe.transitionMenuItem "console.settings.virtual" menuItem}}
|
||||
@item={{menuItem}}
|
||||
@@ -12,7 +12,7 @@
|
||||
>{{menuItem.title}}</Layout::Sidebar::Item>
|
||||
{{/each}}
|
||||
</Layout::Sidebar::Panel>
|
||||
{{#each this.universe.settingsMenuPanels as |menuPanel|}}
|
||||
{{#each this.menuService.settingsMenuPanels as |menuPanel|}}
|
||||
<Layout::Sidebar::Panel @open={{menuPanel.open}} @title={{menuPanel.title}}>
|
||||
{{#each menuPanel.items as |menuItem|}}
|
||||
<Layout::Sidebar::Item
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<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-">
|
||||
{{component @model.component params=@model.componentParams}}
|
||||
<LazyEngineComponent @component={{@model.component}} @params={{@model.componentParams}} />
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{page-title @model.title}}
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
{{component @model.component params=@model.componentParams}}
|
||||
<LazyEngineComponent @component={{@model.component}} @params={{@model.componentParams}} />
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -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>
|
||||
@@ -1,2 +1,2 @@
|
||||
{{page-title @model.title}}
|
||||
{{component @model.component params=@model.componentParams}}
|
||||
<LazyEngineComponent @component={{@model.component}} @params={{@model.componentParams}} />
|
||||
@@ -107,8 +107,8 @@ export function suppressRouterRefreshErrors(application) {
|
||||
// Global error handler for unhandled promise rejections
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
const error = event.reason;
|
||||
if (error?.message?.includes("You didn't provide enough string/numeric parameters to satisfy all of the dynamic segments")) {
|
||||
debug('[Fleetbase Router Patch] Suppressed known Ember route refresh bug:', error.message);
|
||||
if (typeof error?.message === 'string' && error?.message.includes("You didn't provide enough string/numeric parameters to satisfy all of the dynamic segments")) {
|
||||
debug('[Fleetbase Router Patch] Suppressed known Ember route refresh bug: ' + error.message);
|
||||
event.preventDefault(); // Prevent the error from being logged
|
||||
}
|
||||
});
|
||||
@@ -118,8 +118,8 @@ export function suppressRouterRefreshErrors(application) {
|
||||
const originalEmberError = window.Ember.onerror;
|
||||
|
||||
window.Ember.onerror = function (error) {
|
||||
if (error?.message?.includes("You didn't provide enough string/numeric parameters to satisfy all of the dynamic segments")) {
|
||||
debug('[Fleetbase Router Patch] Suppressed known Ember route refresh bug:', error.message);
|
||||
if (typeof error?.message === 'string' && error?.message.includes("You didn't provide enough string/numeric parameters to satisfy all of the dynamic segments")) {
|
||||
debug('[Fleetbase Router Patch] Suppressed known Ember route refresh bug: ' + error.message);
|
||||
return; // Suppress the error
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,13 @@ const RUNTIME_CONFIG_MAP = {
|
||||
EXTENSIONS: 'APP.extensions',
|
||||
};
|
||||
|
||||
/**
|
||||
* Cache key for localStorage
|
||||
*/
|
||||
const CACHE_KEY = 'fleetbase_runtime_config';
|
||||
const CACHE_VERSION_KEY = 'fleetbase_runtime_config_version';
|
||||
const CACHE_TTL = 1000 * 60 * 60; // 1 hour
|
||||
|
||||
/**
|
||||
* Coerce and sanitize runtime config values based on key.
|
||||
*
|
||||
@@ -53,32 +60,130 @@ export function applyRuntimeConfig(rawConfig = {}) {
|
||||
const coercedValue = coerceValue(key, value);
|
||||
set(config, configPath, coercedValue);
|
||||
} else {
|
||||
debug(`[runtime-config] Ignored unknown key: ${key}`);
|
||||
debug(`[Runtime Config] Ignored unknown key: ${key}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and apply runtime config.
|
||||
* Get cached config from localStorage
|
||||
*
|
||||
* @returns {Object|null} Cached config or null
|
||||
*/
|
||||
function getCachedConfig() {
|
||||
try {
|
||||
const cached = localStorage.getItem(CACHE_KEY);
|
||||
const cachedVersion = localStorage.getItem(CACHE_VERSION_KEY);
|
||||
|
||||
if (!cached || !cachedVersion) {
|
||||
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;
|
||||
|
||||
// Check if cache is still valid (within TTL)
|
||||
if (cacheAge > CACHE_TTL) {
|
||||
debug('[Runtime Config] Cache expired');
|
||||
return null;
|
||||
}
|
||||
|
||||
debug(`[Runtime Config] Using cached config (age: ${Math.round(cacheAge / 1000)}s)`);
|
||||
return cacheData.config;
|
||||
} catch (e) {
|
||||
debug(`[Runtime Config] Failed to read cache: ${e.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save config to localStorage cache
|
||||
*
|
||||
* @param {Object} config Config object
|
||||
*/
|
||||
function setCachedConfig(runtimeConfig) {
|
||||
try {
|
||||
const cacheData = {
|
||||
config: runtimeConfig,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached config
|
||||
*
|
||||
* @export
|
||||
* @return {void}
|
||||
*/
|
||||
export function clearRuntimeConfigCache() {
|
||||
try {
|
||||
localStorage.removeItem(CACHE_KEY);
|
||||
localStorage.removeItem(CACHE_VERSION_KEY);
|
||||
debug('[Runtime Config] Cache cleared');
|
||||
} catch (e) {
|
||||
debug(`[Runtime Config] Failed to clear cache: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and apply runtime config with localStorage caching.
|
||||
*
|
||||
* Strategy:
|
||||
* 1. Check localStorage cache first (instant, no HTTP request)
|
||||
* 2. If cache hit and valid, use it immediately
|
||||
* 3. If cache miss, fetch from server and cache the result
|
||||
* 4. Cache is valid for 1 hour
|
||||
*
|
||||
* @export
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export default async function loadRuntimeConfig() {
|
||||
if (config.APP.disableRuntimeConfig) {
|
||||
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 {
|
||||
const response = await fetch(`/fleetbase.config.json?_t=${Date.now()}`, { cache: 'no-cache' });
|
||||
const startTime = performance.now();
|
||||
const response = await fetch('/fleetbase.config.json', {
|
||||
cache: 'default', // Use browser cache if available
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
debug('No fleetbase.config.json found, using built-in config defaults');
|
||||
debug('[Runtime Config] No fleetbase.config.json found, using built-in config defaults');
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeConfig = await response.json();
|
||||
const endTime = performance.now();
|
||||
|
||||
debug(`[Runtime Config] Fetched from server in ${(endTime - startTime).toFixed(2)}ms`);
|
||||
|
||||
// Apply and cache
|
||||
applyRuntimeConfig(runtimeConfig);
|
||||
setCachedConfig(runtimeConfig);
|
||||
} catch (e) {
|
||||
debug(`Failed to load runtime config : ${e.message}`);
|
||||
debug(`[Runtime Config] Failed to load runtime config: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ module.exports = function (/* environment */) {
|
||||
* @type {String?}
|
||||
* @default "null"
|
||||
*/
|
||||
fallbackLocale: 'en-us',
|
||||
fallbackLocale: 'en-US',
|
||||
|
||||
/**
|
||||
* Path where translations are stored. This is relative to the project root.
|
||||
|
||||
@@ -21,9 +21,9 @@ module.exports = function (environment) {
|
||||
},
|
||||
|
||||
APP: {
|
||||
autoboot: false,
|
||||
autoboot: true,
|
||||
extensions: asArray(getenv('EXTENSIONS')),
|
||||
disableRuntimeConfig: toBoolean(getenv('DISABLE_RUNTIME_CONFIG')),
|
||||
disableRuntimeConfig: toBoolean(getenv('DISABLE_RUNTIME_CONFIG', environment === 'production')),
|
||||
},
|
||||
|
||||
API: {
|
||||
|
||||
@@ -2,26 +2,17 @@
|
||||
|
||||
/** eslint-disable node/no-unpublished-require */
|
||||
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
|
||||
const FleetbaseExtensionsIndexer = require('fleetbase-extensions-indexer');
|
||||
const Funnel = require('broccoli-funnel');
|
||||
const writeFile = require('broccoli-file-creator');
|
||||
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 mergeTrees = require('broccoli-merge-trees');
|
||||
const toBoolean = require('./config/utils/to-boolean');
|
||||
const environment = process.env.EMBER_ENV;
|
||||
|
||||
module.exports = function (defaults) {
|
||||
const app = new EmberApp(defaults, {
|
||||
storeConfigInMeta: false,
|
||||
|
||||
fingerprint: {
|
||||
exclude: ['leaflet/', 'leaflet-images/', 'socketcluster-client.min.js'],
|
||||
exclude: ['leaflet/', 'leaflet-images/', 'socketcluster-client.min.js', 'fleetbase.config.json', 'extensions.json'],
|
||||
},
|
||||
|
||||
liveReload: {
|
||||
@@ -30,31 +21,12 @@ module.exports = function (defaults) {
|
||||
},
|
||||
},
|
||||
|
||||
'ember-simple-auth': {
|
||||
useSessionSetupMethod: true,
|
||||
intl: {
|
||||
silent: 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')],
|
||||
},
|
||||
'ember-simple-auth': {
|
||||
useSessionSetupMethod: true,
|
||||
},
|
||||
|
||||
babel: {
|
||||
@@ -62,7 +34,6 @@ module.exports = function (defaults) {
|
||||
},
|
||||
});
|
||||
|
||||
let extensions = new FleetbaseExtensionsIndexer();
|
||||
let runtimeConfigTree;
|
||||
if (toBoolean(process.env.DISABLE_RUNTIME_CONFIG)) {
|
||||
runtimeConfigTree = writeFile('fleetbase.config.json', '{}');
|
||||
@@ -73,5 +44,5 @@ module.exports = function (defaults) {
|
||||
});
|
||||
}
|
||||
|
||||
return app.toTree([extensions, runtimeConfigTree].filter(Boolean));
|
||||
return app.toTree([runtimeConfigTree].filter(Boolean));
|
||||
};
|
||||
|
||||
376
console/lib/fleetbase-extensions-generator/index.js
Normal file
376
console/lib/fleetbase-extensions-generator/index.js
Normal file
@@ -0,0 +1,376 @@
|
||||
/* eslint-env node */
|
||||
'use strict';
|
||||
|
||||
const fg = require('fast-glob');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const recast = require('recast');
|
||||
const babelParser = require('recast/parsers/babel');
|
||||
const builders = recast.types.builders;
|
||||
const chokidar = require('chokidar');
|
||||
|
||||
module.exports = {
|
||||
name: require('./package').name,
|
||||
|
||||
getGeneratedFileHeader() {
|
||||
const year = new Date().getFullYear();
|
||||
return `/**
|
||||
* ███████╗██╗ ███████╗███████╗████████╗██████╗ █████╗ ███████╗███████╗
|
||||
* ██╔════╝██║ ██╔════╝██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██╔════╝██╔════╝
|
||||
* █████╗ ██║ █████╗ █████╗ ██║ ██████╔╝███████║███████╗█████╗
|
||||
* ██╔══╝ ██║ ██╔══╝ ██╔══╝ ██║ ██╔══██╗██╔══██║╚════██║██╔══╝
|
||||
* ██║ ███████╗███████╗███████╗ ██║ ██████╔╝██║ ██║███████║███████╗
|
||||
* ╚═╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝
|
||||
*
|
||||
* AUTO-GENERATED FILE - DO NOT EDIT
|
||||
*
|
||||
* This file is automatically generated by the Fleetbase extension build system.
|
||||
* Any manual changes will be overwritten on the next build.
|
||||
*
|
||||
* @generated
|
||||
* @copyright © ${year} Fleetbase Pte Ltd. All rights reserved.
|
||||
* @license AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
`;
|
||||
},
|
||||
|
||||
included(app) {
|
||||
this._super.included.apply(this, arguments);
|
||||
|
||||
console.log('\n' + '/'.repeat(70));
|
||||
console.log('[Fleetbase] Extension Build System');
|
||||
console.log('/'.repeat(70));
|
||||
|
||||
// Generate files on startup
|
||||
this.generateExtensionFiles();
|
||||
|
||||
// Watch for changes in development
|
||||
this.watchExtensionFiles();
|
||||
},
|
||||
|
||||
async generateExtensionFiles() {
|
||||
// Clean up old/stale extensions directory before generating new files
|
||||
const extensionsDir = path.join(this.project.root, 'app', 'extensions');
|
||||
if (fs.existsSync(extensionsDir)) {
|
||||
console.log('[Fleetbase] Cleaning up old extensions directory...');
|
||||
fs.rmSync(extensionsDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const extensions = await this.getExtensions();
|
||||
|
||||
if (extensions.length === 0) {
|
||||
console.log('[Fleetbase] No extensions found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[Fleetbase] Discovered ${extensions.length} extension(s)`);
|
||||
extensions.forEach((ext) => {
|
||||
console.log(`[Fleetbase] - ${ext.name} (v${ext.version})`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
// Generate extension shims
|
||||
this.generateExtensionShims(extensions);
|
||||
|
||||
// Generate extension loaders
|
||||
this.generateExtensionLoaders(extensions);
|
||||
|
||||
// Generate router
|
||||
this.generateRouter(extensions);
|
||||
|
||||
// Generate manifest
|
||||
this.generateExtensionsManifest(extensions);
|
||||
},
|
||||
|
||||
getExtensions() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const extensions = [];
|
||||
const seenPackages = new Set();
|
||||
const cwd = this.project.root;
|
||||
|
||||
return fg(['node_modules/*/package.json', 'node_modules/*/*/package.json'], { cwd })
|
||||
.then((results) => {
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const packagePath = path.join(cwd, results[i]);
|
||||
const packageJson = fs.readFileSync(packagePath);
|
||||
let packageData = null;
|
||||
|
||||
try {
|
||||
packageData = JSON.parse(packageJson);
|
||||
} catch (e) {
|
||||
console.warn(`Could not parse package.json at ${packagePath}:`, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!packageData || !packageData.keywords || !packageData.keywords.includes('fleetbase-extension') || !packageData.keywords.includes('ember-engine')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we've seen this package before, skip it
|
||||
if (seenPackages.has(packageData.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seenPackages.add(packageData.name);
|
||||
extensions.push(this.only(packageData, ['name', 'description', 'version', 'fleetbase', 'keywords', 'license', 'repository']));
|
||||
}
|
||||
|
||||
resolve(extensions);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
},
|
||||
|
||||
only(subject, props = []) {
|
||||
const keys = Object.keys(subject);
|
||||
const result = {};
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
|
||||
if (props.includes(key)) {
|
||||
result[key] = subject[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
getExtensionMountPath(extensionName) {
|
||||
let extensionNameSegments = extensionName.split('/');
|
||||
let mountName = extensionNameSegments[1];
|
||||
|
||||
if (typeof mountName !== 'string') {
|
||||
mountName = extensionNameSegments[0];
|
||||
}
|
||||
|
||||
return mountName.replace('-engine', '');
|
||||
},
|
||||
|
||||
generateExtensionShims(extensions) {
|
||||
const extensionsDir = path.join(this.project.root, 'app', 'extensions');
|
||||
|
||||
if (!fs.existsSync(extensionsDir)) {
|
||||
fs.mkdirSync(extensionsDir, { recursive: true });
|
||||
}
|
||||
|
||||
extensions.forEach((extension) => {
|
||||
const extensionPath = path.join(this.project.root, 'node_modules', extension.name, 'addon', 'extension.js');
|
||||
|
||||
if (!fs.existsSync(extensionPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const extensionContent = fs.readFileSync(extensionPath, 'utf8');
|
||||
const mountPath = extension.fleetbase?.route || this.getExtensionMountPath(extension.name);
|
||||
const shimFile = path.join(extensionsDir, `${mountPath}.js`);
|
||||
const fileContent = this.getGeneratedFileHeader() + extensionContent;
|
||||
|
||||
fs.writeFileSync(shimFile, fileContent, 'utf8');
|
||||
console.log(`[Fleetbase] ✓ Generated app/extensions/${mountPath}.js`);
|
||||
});
|
||||
},
|
||||
|
||||
generateExtensionLoaders(extensions) {
|
||||
const extensionsDir = path.join(this.project.root, 'app', 'extensions');
|
||||
|
||||
if (!fs.existsSync(extensionsDir)) {
|
||||
fs.mkdirSync(extensionsDir, { recursive: true });
|
||||
}
|
||||
|
||||
const imports = [];
|
||||
const loaders = {};
|
||||
|
||||
extensions.forEach((extension) => {
|
||||
const extensionPath = path.join(this.project.root, 'node_modules', extension.name, 'addon', 'extension.js');
|
||||
|
||||
if (!fs.existsSync(extensionPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mountPath = extension.fleetbase?.route || this.getExtensionMountPath(extension.name);
|
||||
const camelCaseName = mountPath.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
||||
|
||||
imports.push(`import ${camelCaseName} from './${mountPath}';`);
|
||||
loaders[extension.name] = `() => ${camelCaseName}`;
|
||||
});
|
||||
|
||||
const loadersContent =
|
||||
this.getGeneratedFileHeader() +
|
||||
`${imports.join('\n')}
|
||||
|
||||
export const EXTENSION_LOADERS = {
|
||||
${Object.entries(loaders)
|
||||
.map(([name, loader]) => ` '${name}': ${loader}`)
|
||||
.join(',\n')}
|
||||
};
|
||||
|
||||
export const getExtensionLoader = (packageName) => {
|
||||
return EXTENSION_LOADERS[packageName];
|
||||
};
|
||||
|
||||
export default getExtensionLoader;
|
||||
`;
|
||||
|
||||
const loadersFile = path.join(extensionsDir, 'index.js');
|
||||
fs.writeFileSync(loadersFile, loadersContent, 'utf8');
|
||||
console.log(`[Fleetbase] ✓ Generated app/extensions/index.js`);
|
||||
},
|
||||
|
||||
generateRouter(extensions) {
|
||||
const consoleExtensions = extensions.filter((extension) => !extension.fleetbase || extension.fleetbase.mount !== 'root');
|
||||
const rootExtensions = extensions.filter((extension) => extension.fleetbase && extension.fleetbase.mount === 'root');
|
||||
const routerMapPath = path.join(this.project.root, 'router.map.js');
|
||||
const routerFileContents = fs.readFileSync(routerMapPath, 'utf-8');
|
||||
const ast = recast.parse(routerFileContents, { parser: babelParser });
|
||||
|
||||
recast.visit(ast, {
|
||||
visitCallExpression(path) {
|
||||
if (path.value.type === 'CallExpression' && path.value.callee.property.name === 'route' && path.value.arguments[0].value === 'console') {
|
||||
let functionExpression;
|
||||
|
||||
// Find the function expression
|
||||
path.value.arguments.forEach((arg) => {
|
||||
if (arg.type === 'FunctionExpression') {
|
||||
functionExpression = arg;
|
||||
}
|
||||
});
|
||||
|
||||
if (functionExpression) {
|
||||
// Check and add the new engine mounts
|
||||
consoleExtensions.forEach((extension) => {
|
||||
const mountPath = module.exports.getExtensionMountPath(extension.name);
|
||||
let route = mountPath;
|
||||
|
||||
if (extension.fleetbase && extension.fleetbase.route) {
|
||||
route = extension.fleetbase.route;
|
||||
}
|
||||
|
||||
// Check if engine is already mounted
|
||||
const isMounted = functionExpression.body.body.some((expressionStatement) => {
|
||||
return expressionStatement.expression.arguments[0].value === extension.name;
|
||||
});
|
||||
|
||||
// If not mounted, append to the function body
|
||||
if (!isMounted) {
|
||||
functionExpression.body.body.push(
|
||||
builders.expressionStatement(
|
||||
builders.callExpression(builders.memberExpression(builders.thisExpression(), builders.identifier('mount')), [
|
||||
builders.literal(extension.name),
|
||||
builders.objectExpression([
|
||||
builders.property('init', builders.identifier('as'), builders.literal(route)),
|
||||
builders.property('init', builders.identifier('path'), builders.literal(route)),
|
||||
]),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (path.value.type === 'CallExpression' && path.value.callee.property.name === 'map') {
|
||||
let functionExpression;
|
||||
|
||||
path.value.arguments.forEach((arg) => {
|
||||
if (arg.type === 'FunctionExpression') {
|
||||
functionExpression = arg;
|
||||
}
|
||||
});
|
||||
|
||||
if (functionExpression) {
|
||||
rootExtensions.forEach((extension) => {
|
||||
const mountPath = module.exports.getExtensionMountPath(extension.name);
|
||||
let route = mountPath;
|
||||
|
||||
if (extension.fleetbase && extension.fleetbase.route) {
|
||||
route = extension.fleetbase.route;
|
||||
}
|
||||
|
||||
const isMounted = functionExpression.body.body.some((expressionStatement) => {
|
||||
return expressionStatement.expression.arguments[0].value === extension.name;
|
||||
});
|
||||
|
||||
if (!isMounted) {
|
||||
functionExpression.body.body.push(
|
||||
builders.expressionStatement(
|
||||
builders.callExpression(builders.memberExpression(builders.thisExpression(), builders.identifier('mount')), [
|
||||
builders.literal(extension.name),
|
||||
builders.objectExpression([
|
||||
builders.property('init', builders.identifier('as'), builders.literal(route)),
|
||||
builders.property('init', builders.identifier('path'), builders.literal(route)),
|
||||
]),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.traverse(path);
|
||||
},
|
||||
});
|
||||
|
||||
const output = recast.print(ast, { quote: 'single' }).code;
|
||||
const routerFile = path.join(this.project.root, 'app/router.js');
|
||||
const fileContent = this.getGeneratedFileHeader() + output;
|
||||
fs.writeFileSync(routerFile, fileContent);
|
||||
console.log(`[Fleetbase] ✓ Generated app/router.js`);
|
||||
},
|
||||
|
||||
generateExtensionsManifest(extensions) {
|
||||
const publicDir = path.join(this.project.root, 'public');
|
||||
|
||||
if (!fs.existsSync(publicDir)) {
|
||||
fs.mkdirSync(publicDir, { recursive: true });
|
||||
}
|
||||
|
||||
const manifest = extensions.map((ext) => ({
|
||||
name: ext.name,
|
||||
version: ext.version,
|
||||
route: ext.fleetbase?.route,
|
||||
}));
|
||||
|
||||
const manifestFile = path.join(publicDir, 'extensions.json');
|
||||
fs.writeFileSync(manifestFile, JSON.stringify(manifest, null, 2), 'utf8');
|
||||
console.log(`[Fleetbase] ✓ Generated public/extensions.json`);
|
||||
},
|
||||
|
||||
watchExtensionFiles() {
|
||||
const isDevelopment = process.env.EMBER_ENV !== 'production';
|
||||
|
||||
if (!isDevelopment) {
|
||||
return;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
|
||||
this.getExtensions().then((extensions) => {
|
||||
const extensionFiles = [];
|
||||
|
||||
extensions.forEach((extension) => {
|
||||
const extensionPath = path.join(self.project.root, 'node_modules', extension.name, 'addon', 'extension.js');
|
||||
if (fs.existsSync(extensionPath)) {
|
||||
extensionFiles.push(extensionPath);
|
||||
}
|
||||
});
|
||||
|
||||
if (extensionFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const watcher = chokidar.watch(extensionFiles, {
|
||||
persistent: true,
|
||||
ignoreInitial: true,
|
||||
});
|
||||
|
||||
watcher.on('change', (filePath) => {
|
||||
console.log(`\n[Fleetbase] Extension file changed: ${path.basename(filePath)}`);
|
||||
console.log('[Fleetbase] Regenerating extension files...\n');
|
||||
self.generateExtensionFiles();
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
15
console/lib/fleetbase-extensions-generator/package.json
Normal file
15
console/lib/fleetbase-extensions-generator/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "fleetbase-extensions-generator",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"keywords": [
|
||||
"ember-addon"
|
||||
],
|
||||
"ember-addon": {
|
||||
"configPath": "tests/dummy/config"
|
||||
},
|
||||
"dependencies": {
|
||||
"broccoli-funnel": "^5.0.2",
|
||||
"broccoli-merge-trees": "^5.2.1"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fleetbase/console",
|
||||
"version": "0.7.18",
|
||||
"version": "0.7.23",
|
||||
"private": true,
|
||||
"description": "Modular logistics and supply chain operating system (LSOS)",
|
||||
"repository": "https://github.com/fleetbase/fleetbase",
|
||||
@@ -11,8 +11,7 @@
|
||||
"test": "tests"
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "node prebuild.js",
|
||||
"build": "pnpm run prebuild && ember build",
|
||||
"build": "ember build",
|
||||
"lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\"",
|
||||
"lint:css": "stylelint \"**/*.css\"",
|
||||
"lint:css:fix": "concurrently \"npm:lint:css -- --fix\"",
|
||||
@@ -22,22 +21,27 @@
|
||||
"lint:js": "eslint . --cache",
|
||||
"lint:js:fix": "eslint . --fix",
|
||||
"lint:intl": "fleetbase-intl-lint",
|
||||
"start": "pnpm run prebuild && ember serve",
|
||||
"start:dev": "pnpm run prebuild && ember serve --environment development",
|
||||
"start": "ember serve",
|
||||
"start:dev": "ember serve --environment development",
|
||||
"test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\"",
|
||||
"test:ember": "ember test"
|
||||
},
|
||||
"ember-addon": {
|
||||
"paths": [
|
||||
"lib/fleetbase-extensions-generator"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@ember/legacy-built-in-components": "^0.4.2",
|
||||
"@fleetbase/dev-engine": "^0.2.10",
|
||||
"@fleetbase/ember-core": "latest",
|
||||
"@fleetbase/ember-ui": "latest",
|
||||
"@fleetbase/fleetops-data": "latest",
|
||||
"@fleetbase/fleetops-engine": "^0.6.25",
|
||||
"@fleetbase/iam-engine": "^0.1.4",
|
||||
"@fleetbase/dev-engine": "^0.2.12",
|
||||
"@fleetbase/ember-core": "^0.3.9",
|
||||
"@fleetbase/ember-ui": "^0.3.15",
|
||||
"@fleetbase/fleetops-data": "^0.1.24",
|
||||
"@fleetbase/fleetops-engine": "^0.6.31",
|
||||
"@fleetbase/iam-engine": "^0.1.6",
|
||||
"@fleetbase/leaflet-routing-machine": "^3.2.17",
|
||||
"@fleetbase/registry-bridge-engine": "^0.1.0",
|
||||
"@fleetbase/storefront-engine": "^0.4.6",
|
||||
"@fleetbase/registry-bridge-engine": "^0.1.2",
|
||||
"@fleetbase/storefront-engine": "^0.4.10",
|
||||
"@formatjs/intl-datetimeformat": "^6.18.2",
|
||||
"@formatjs/intl-numberformat": "^8.15.6",
|
||||
"@formatjs/intl-pluralrules": "^5.4.6",
|
||||
@@ -78,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",
|
||||
@@ -87,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",
|
||||
@@ -143,9 +150,9 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@fleetbase/ember-core": "latest",
|
||||
"@fleetbase/ember-ui": "latest",
|
||||
"@fleetbase/fleetops-data": "latest"
|
||||
"@fleetbase/ember-core": "^0.3.9",
|
||||
"@fleetbase/ember-ui": "^0.3.15",
|
||||
"@fleetbase/fleetops-data": "^0.1.24"
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
@@ -162,5 +169,6 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903"
|
||||
}
|
||||
|
||||
2672
console/pnpm-lock.yaml
generated
2672
console/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,176 +0,0 @@
|
||||
const fg = require('fast-glob');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const recast = require('recast');
|
||||
const babelParser = require('recast/parsers/babel');
|
||||
const builders = recast.types.builders;
|
||||
|
||||
function getExtensionMountPath(extensionName) {
|
||||
let extensionNameSegments = extensionName.split('/');
|
||||
let mountName = extensionNameSegments[1];
|
||||
|
||||
if (typeof mountName !== 'string') {
|
||||
mountName = extensionNameSegments[0];
|
||||
}
|
||||
|
||||
return mountName.replace('-engine', '');
|
||||
}
|
||||
|
||||
function only(subject, props = []) {
|
||||
const keys = Object.keys(subject);
|
||||
const result = {};
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
|
||||
if (props.includes(key)) {
|
||||
result[key] = subject[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getExtensions() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const extensions = [];
|
||||
const seenPackages = new Set();
|
||||
|
||||
return fg(['node_modules/*/package.json', 'node_modules/*/*/package.json'])
|
||||
.then((results) => {
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const packagePath = results[i];
|
||||
const packageJson = fs.readFileSync(packagePath);
|
||||
let packageData = null;
|
||||
|
||||
try {
|
||||
packageData = JSON.parse(packageJson);
|
||||
} catch (e) {
|
||||
console.warn(`Could not parse package.json at ${packagePath}:`, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!packageData || !packageData.keywords || !packageData.keywords.includes('fleetbase-extension') || !packageData.keywords.includes('ember-engine')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we've seen this package before, skip it
|
||||
if (seenPackages.has(packageData.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seenPackages.add(packageData.name);
|
||||
extensions.push(only(packageData, ['name', 'description', 'version', 'fleetbase', 'keywords', 'license', 'repository']));
|
||||
}
|
||||
|
||||
resolve(extensions);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
function getRouterFileContents() {
|
||||
const routerFilePath = path.join(__dirname, 'router.map.js');
|
||||
const routerFileContents = fs.readFileSync(routerFilePath, 'utf-8');
|
||||
|
||||
return routerFileContents;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const extensions = await getExtensions();
|
||||
const consoleExtensions = extensions.filter((extension) => !extension.fleetbase || extension.fleetbase.mount !== 'root');
|
||||
const rootExtensions = extensions.filter((extension) => extension.fleetbase && extension.fleetbase.mount === 'root');
|
||||
const routerFileContents = getRouterFileContents();
|
||||
const ast = recast.parse(routerFileContents, { parser: babelParser });
|
||||
|
||||
recast.visit(ast, {
|
||||
visitCallExpression(path) {
|
||||
if (path.value.type === 'CallExpression' && path.value.callee.property.name === 'route' && path.value.arguments[0].value === 'console') {
|
||||
let functionExpression;
|
||||
|
||||
// Find the function expression
|
||||
path.value.arguments.forEach((arg) => {
|
||||
if (arg.type === 'FunctionExpression') {
|
||||
functionExpression = arg;
|
||||
}
|
||||
});
|
||||
if (functionExpression) {
|
||||
// Check and add the new engine mounts
|
||||
consoleExtensions.forEach((extension) => {
|
||||
const mountPath = getExtensionMountPath(extension.name);
|
||||
let route = mountPath;
|
||||
|
||||
if (extension.fleetbase && extension.fleetbase.route) {
|
||||
route = extension.fleetbase.route;
|
||||
}
|
||||
|
||||
// Check if engine is already mounted
|
||||
const isMounted = functionExpression.body.body.some((expressionStatement) => {
|
||||
return expressionStatement.expression.arguments[0].value === extension.name;
|
||||
});
|
||||
|
||||
// If not mounted, append to the function body
|
||||
if (!isMounted) {
|
||||
functionExpression.body.body.push(
|
||||
builders.expressionStatement(
|
||||
builders.callExpression(builders.memberExpression(builders.thisExpression(), builders.identifier('mount')), [
|
||||
builders.literal(extension.name),
|
||||
builders.objectExpression([
|
||||
builders.property('init', builders.identifier('as'), builders.literal(route)),
|
||||
builders.property('init', builders.identifier('path'), builders.literal(route)),
|
||||
]),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// console.log(path.value.callee.property.name);
|
||||
if (path.value.type === 'CallExpression' && path.value.callee.property.name === 'map') {
|
||||
let functionExpression;
|
||||
|
||||
path.value.arguments.forEach((arg) => {
|
||||
if (arg.type === 'FunctionExpression') {
|
||||
functionExpression = arg;
|
||||
}
|
||||
});
|
||||
|
||||
if (functionExpression) {
|
||||
rootExtensions.forEach((extension) => {
|
||||
const mountPath = getExtensionMountPath(extension.name);
|
||||
let route = mountPath;
|
||||
|
||||
if (extension.fleetbase && extension.fleetbase.route) {
|
||||
route = extension.fleetbase.route;
|
||||
}
|
||||
|
||||
const isMounted = functionExpression.body.body.some((expressionStatement) => {
|
||||
return expressionStatement.expression.arguments[0].value === extension.name;
|
||||
});
|
||||
|
||||
if (!isMounted) {
|
||||
functionExpression.body.body.push(
|
||||
builders.expressionStatement(
|
||||
builders.callExpression(builders.memberExpression(builders.thisExpression(), builders.identifier('mount')), [
|
||||
builders.literal(extension.name),
|
||||
builders.objectExpression([
|
||||
builders.property('init', builders.identifier('as'), builders.literal(route)),
|
||||
builders.property('init', builders.identifier('path'), builders.literal(route)),
|
||||
]),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.traverse(path);
|
||||
},
|
||||
});
|
||||
|
||||
const output = recast.print(ast, { quote: 'single' }).code;
|
||||
fs.writeFileSync(path.join(__dirname, 'app/router.js'), output);
|
||||
})();
|
||||
@@ -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"
|
||||
|
||||
@@ -8,6 +8,7 @@ export default class Router extends EmberRouter {
|
||||
|
||||
Router.map(function () {
|
||||
this.route('virtual', { path: '/:slug' });
|
||||
this.route('install');
|
||||
this.route('onboard', function () {
|
||||
this.route('index', { path: '/' });
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
12
console/tests/unit/controllers/virtual-test.js
Normal file
12
console/tests/unit/controllers/virtual-test.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Controller | virtual', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:virtual');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import Application from '@ember/application';
|
||||
|
||||
import config from '@fleetbase/console/config/environment';
|
||||
import { initialize } from '@fleetbase/console/instance-initializers/initialize-registries';
|
||||
import { module, test } from 'qunit';
|
||||
import Resolver from 'ember-resolver';
|
||||
import { run } from '@ember/runloop';
|
||||
|
||||
module('Unit | Instance Initializer | initialize-registries', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.TestApplication = class TestApplication extends Application {
|
||||
modulePrefix = config.modulePrefix;
|
||||
podModulePrefix = config.podModulePrefix;
|
||||
Resolver = Resolver;
|
||||
};
|
||||
|
||||
this.TestApplication.instanceInitializer({
|
||||
name: 'initializer under test',
|
||||
initialize,
|
||||
});
|
||||
|
||||
this.application = this.TestApplication.create({
|
||||
autoboot: false,
|
||||
});
|
||||
|
||||
this.instance = this.application.buildInstance();
|
||||
});
|
||||
hooks.afterEach(function () {
|
||||
run(this.instance, 'destroy');
|
||||
run(this.application, 'destroy');
|
||||
});
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it works', async function (assert) {
|
||||
await this.instance.boot();
|
||||
|
||||
assert.ok(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import Application from '@ember/application';
|
||||
|
||||
import config from '@fleetbase/console/config/environment';
|
||||
import { initialize } from '@fleetbase/console/instance-initializers/initialize-widgets';
|
||||
import { module, test } from 'qunit';
|
||||
import Resolver from 'ember-resolver';
|
||||
import { run } from '@ember/runloop';
|
||||
|
||||
module('Unit | Instance Initializer | initialize-widgets', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.TestApplication = class TestApplication extends Application {
|
||||
modulePrefix = config.modulePrefix;
|
||||
podModulePrefix = config.podModulePrefix;
|
||||
Resolver = Resolver;
|
||||
};
|
||||
|
||||
this.TestApplication.instanceInitializer({
|
||||
name: 'initializer under test',
|
||||
initialize,
|
||||
});
|
||||
|
||||
this.application = this.TestApplication.create({
|
||||
autoboot: false,
|
||||
});
|
||||
|
||||
this.instance = this.application.buildInstance();
|
||||
});
|
||||
hooks.afterEach(function () {
|
||||
run(this.instance, 'destroy');
|
||||
run(this.application, 'destroy');
|
||||
});
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it works', async function (assert) {
|
||||
await this.instance.boot();
|
||||
|
||||
assert.ok(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import Application from '@ember/application';
|
||||
|
||||
import config from '@fleetbase/console/config/environment';
|
||||
import { initialize } from '@fleetbase/console/instance-initializers/setup-extensions';
|
||||
import { module, test } from 'qunit';
|
||||
import Resolver from 'ember-resolver';
|
||||
import { run } from '@ember/runloop';
|
||||
|
||||
module('Unit | Instance Initializer | setup-extensions', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.TestApplication = class TestApplication extends Application {
|
||||
modulePrefix = config.modulePrefix;
|
||||
podModulePrefix = config.podModulePrefix;
|
||||
Resolver = Resolver;
|
||||
};
|
||||
|
||||
this.TestApplication.instanceInitializer({
|
||||
name: 'initializer under test',
|
||||
initialize,
|
||||
});
|
||||
|
||||
this.application = this.TestApplication.create({
|
||||
autoboot: false,
|
||||
});
|
||||
|
||||
this.instance = this.application.buildInstance();
|
||||
});
|
||||
hooks.afterEach(function () {
|
||||
run(this.instance, 'destroy');
|
||||
run(this.application, 'destroy');
|
||||
});
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it works', async function (assert) {
|
||||
await this.instance.boot();
|
||||
|
||||
assert.ok(true);
|
||||
});
|
||||
});
|
||||
@@ -454,6 +454,7 @@ dashboard-widget-panel:
|
||||
{widgetName} Widget
|
||||
select-widgets: Select Widgets
|
||||
close-and-save: Close and Save
|
||||
filter-widgets: Filter widgets by keyword
|
||||
|
||||
filters-picker:
|
||||
filters: Filters
|
||||
|
||||
821
console/translations/es-mx.yaml
Normal file
821
console/translations/es-mx.yaml
Normal file
@@ -0,0 +1,821 @@
|
||||
app:
|
||||
name: Fleetbase
|
||||
|
||||
common:
|
||||
new: Nuevo
|
||||
create: Crear
|
||||
add: Agregar
|
||||
edit: Editar
|
||||
update: Actualizar
|
||||
save: Guardar
|
||||
save-changes: Guardar Cambios
|
||||
delete: Eliminar
|
||||
delete-selected: Eliminar Seleccionados
|
||||
delete-selected-count: Eliminar {count} Seleccionados
|
||||
your-profile: Tu Perfil
|
||||
date-of-birth: Fecha de Nacimiento
|
||||
organization: Organización
|
||||
two-factor: Dos Factores
|
||||
remove: Remover
|
||||
cancel: Cancelar
|
||||
confirm: Confirmar
|
||||
close: Cerrar
|
||||
open: Abrir
|
||||
view: Ver
|
||||
preview: Vista previa
|
||||
upload: Subir
|
||||
download: Descargar
|
||||
import: Importar
|
||||
export: Exportar
|
||||
print: Imprimir
|
||||
duplicate: Duplicar
|
||||
copy: Copiar
|
||||
paste: Pegar
|
||||
share: Compartir
|
||||
refresh: Actualizar
|
||||
reset: Restablecer
|
||||
retry: Reintentar
|
||||
back: Atrás
|
||||
next: Siguiente
|
||||
previous: Anterior
|
||||
submit: Enviar
|
||||
apply: Aplicar
|
||||
continue: Continuar
|
||||
proceed: Proceder
|
||||
select: Seleccionar
|
||||
deselect: Deseleccionar
|
||||
search: Buscar
|
||||
filter: Filtrar
|
||||
sort: Ordenar
|
||||
view-all: Ver Todo
|
||||
clear: Limpiar
|
||||
done: Hecho
|
||||
finish: Finalizar
|
||||
skip: Omitir
|
||||
method: Método
|
||||
bulk-delete: Eliminación Masiva
|
||||
bulk-delete-resource: Eliminación Masiva de {resource}
|
||||
bulk-cancel: Cancelación masiva
|
||||
bulk-cancel-resource: Cancelación masiva de {resource}
|
||||
bulk-actions: Acciones masivas
|
||||
column: Columna
|
||||
row: Fila
|
||||
table: Tabla
|
||||
list: Lista
|
||||
grid: Cuadrícula
|
||||
form: Formulario
|
||||
field: Campo
|
||||
section: Sección
|
||||
panel: Panel
|
||||
card: Tarjeta
|
||||
tab: Pestaña
|
||||
modal: Modal
|
||||
dialog: Diálogo
|
||||
menu: Menú
|
||||
dropdown: Desplegable
|
||||
tooltip: Información sobre herramientas
|
||||
sidebar: Barra lateral
|
||||
toolbar: Barra de herramientas
|
||||
footer: Pie de página
|
||||
header: Encabezado
|
||||
title: Título
|
||||
subtitle: Subtítulo
|
||||
description: Descripción
|
||||
placeholder: Marcador de posición
|
||||
label: Etiqueta
|
||||
button: Botón
|
||||
icon: Ícono
|
||||
avatar: Avatar
|
||||
link: Enlace
|
||||
badge: Insignia
|
||||
tag: Etiqueta
|
||||
banner: Banner
|
||||
step: Paso
|
||||
progress: Progreso
|
||||
map: Mapa
|
||||
board: Tablero
|
||||
loading: Cargando
|
||||
loading-resource: Cargando {resource}
|
||||
saving: Guardando
|
||||
processing: Procesando
|
||||
fetching: Obteniendo
|
||||
updating: Actualizando
|
||||
uploading: Subiendo
|
||||
completed: Completado
|
||||
success: Éxito
|
||||
failed: Fallido
|
||||
error: Error
|
||||
warning: Advertencia
|
||||
info: Información
|
||||
ready: Listo
|
||||
activity: Actividad
|
||||
active: Activo
|
||||
inactive: Inactivo
|
||||
enabled: Habilitado
|
||||
disabled: Deshabilitado
|
||||
online: En línea
|
||||
offline: Desconectado
|
||||
pending: Pendiente
|
||||
archived: Archivado
|
||||
hidden: Oculto
|
||||
visible: Visible
|
||||
empty: Vacío
|
||||
not-found: No encontrado
|
||||
no-results: Sin resultados
|
||||
try-again: Intentar de nuevo
|
||||
are-you-sure: ¿Estás seguro?
|
||||
changes-saved: Cambios guardados correctamente.
|
||||
saved-successfully: Cambios guardados correctamente.
|
||||
field-saved: '{field} guardado correctamente.'
|
||||
changes-discarded: Cambios descartados.
|
||||
delete-confirm: ¿Estás seguro de que quieres eliminar este elemento?
|
||||
action-successful: Acción completada con éxito.
|
||||
action-failed: La acción ha fallado. Por favor, inténtalo de nuevo.
|
||||
something-went-wrong: Algo salió mal.
|
||||
please-wait: Por favor espera...
|
||||
sign-in: Iniciar sesión
|
||||
sign-out: Cerrar sesión
|
||||
sign-up: Registrarse
|
||||
log-in: Iniciar sesión
|
||||
log-out: Cerrar sesión
|
||||
register: Registrar
|
||||
forgot-password: ¿Olvidaste tu contraseña?
|
||||
reset-password: Restablecer contraseña
|
||||
change-password: Cambiar contraseña
|
||||
password: Contraseña
|
||||
confirm-password: Confirmar contraseña
|
||||
email: Correo electrónico
|
||||
username: Nombre de usuario
|
||||
remember-me: Recuérdame
|
||||
welcome: Bienvenido
|
||||
welcome-back: Bienvenido de nuevo
|
||||
profile: Perfil
|
||||
account: Cuenta
|
||||
settings: Configuración
|
||||
preferences: Preferencias
|
||||
record: Registro
|
||||
records: Registros
|
||||
item: Elemento
|
||||
items: Elementos
|
||||
entry: Entrada
|
||||
entries: Entradas
|
||||
id: ID
|
||||
name: Nombre
|
||||
type: Tipo
|
||||
category: Categoría
|
||||
overview: Resumen
|
||||
value: Valor
|
||||
amount: Cantidad
|
||||
price: Precio
|
||||
quantity: Cantidad
|
||||
status: Estado
|
||||
date: Fecha
|
||||
date-created: Fecha de creación
|
||||
date-updated: Fecha de actualización
|
||||
time: Hora
|
||||
created-at: Creado en
|
||||
updated-at: Actualizado en
|
||||
expired-at: Expirado en
|
||||
last-seen-at: Última vez visto en
|
||||
last-modified: Última modificación
|
||||
last-modified-data: 'Última modificación: {date}'
|
||||
actions: Acciones
|
||||
details: Detalles
|
||||
notes: Notas
|
||||
reference: Referencia
|
||||
filter-by: Filtrar por
|
||||
filter-by-field: Filtrar por {field}
|
||||
sort-by: Ordenar por
|
||||
ascending: Ascendente
|
||||
descending: Descendente
|
||||
all: Todos
|
||||
none: Ninguno
|
||||
select-all: Seleccionar todo
|
||||
deselect-all: Deseleccionar todo
|
||||
show-more: Mostrar más
|
||||
show-less: Mostrar menos
|
||||
page: Página
|
||||
of: de
|
||||
total: Total
|
||||
items-per-page: Elementos por página
|
||||
showing: Mostrando
|
||||
to: a
|
||||
results: Resultados
|
||||
load-more: Cargar más
|
||||
no-more-results: No hay más resultados
|
||||
today: Hoy
|
||||
yesterday: Ayer
|
||||
tomorrow: Mañana
|
||||
day: Día
|
||||
week: Semana
|
||||
month: Mes
|
||||
year: Año
|
||||
date-range: Rango de fechas
|
||||
start-date: Fecha de inicio
|
||||
end-date: Fecha de fin
|
||||
time-zone: Zona horaria
|
||||
system: Sistema
|
||||
dashboard: Panel
|
||||
home: Inicio
|
||||
analytics: Analíticas
|
||||
reports: Informes
|
||||
logs: Registros
|
||||
help: Ayuda
|
||||
support: Soporte
|
||||
contact: Contacto
|
||||
documentation: Documentación
|
||||
language: Idioma
|
||||
timezone: Zona horaria
|
||||
version: Versión
|
||||
theme: Tema
|
||||
light-mode: Modo claro
|
||||
dark-mode: Modo oscuro
|
||||
update-available: Actualización disponible
|
||||
install-update: Instalar actualización
|
||||
maintenance-mode: Modo mantenimiento
|
||||
notification: Notificación
|
||||
notifications: Notificaciones
|
||||
mark-as-read: Marcar como leído
|
||||
mark-all-as-read: Marcar todo como leído
|
||||
clear-notifications: Limpiar notificaciones
|
||||
company: Empresa
|
||||
companies: Empresas
|
||||
user: Usuario
|
||||
users: Usuarios
|
||||
role: Rol
|
||||
roles: Roles
|
||||
permission: Permiso
|
||||
permissions: Permisos
|
||||
group: Grupo
|
||||
groups: Grupos
|
||||
unauthorized: No autorizado
|
||||
forbidden: Prohibido
|
||||
resource-not-found: Recurso no encontrado
|
||||
server-error: Error del servidor
|
||||
validation-error: Error de validación
|
||||
timeout-error: Tiempo de espera agotado
|
||||
network-error: Error de red
|
||||
unknown-error: Error desconocido
|
||||
file: Archivo
|
||||
files: Archivos
|
||||
folder: Carpeta
|
||||
folders: Carpetas
|
||||
upload-file: Subir archivo
|
||||
upload-files: Subir archivos
|
||||
upload-image: Subir imagen
|
||||
upload-image-supported: Soporta PNG, JPEG y GIF
|
||||
choose-file: Elegir archivo
|
||||
choose-files: Elegir archivos
|
||||
drag-and-drop: Arrastrar y soltar
|
||||
download-file: Descargar archivo
|
||||
file-size: Tamaño del archivo
|
||||
file-type: Tipo de archivo
|
||||
confirm-delete: Confirmar eliminación
|
||||
confirm-action: Confirmar acción
|
||||
confirm-exit: Confirmar salida
|
||||
confirm-and-save-changes: Confirmar y guardar cambios
|
||||
are-you-sure-exit: ¿Estás seguro de que quieres salir?
|
||||
unsaved-changes-warning: Tienes cambios sin guardar.
|
||||
connected: Conectado
|
||||
disconnected: Desconectado
|
||||
reconnecting: Reconectando
|
||||
connection-lost: Conexión perdida
|
||||
connection-restored: Conexión restaurada
|
||||
show: Mostrar
|
||||
hide: Ocultar
|
||||
expand: Expandir
|
||||
collapse: Colapsar
|
||||
enable: Habilitar
|
||||
disable: Deshabilitar
|
||||
minimize: Minimizar
|
||||
maximize: Maximizar
|
||||
restore: Restaurar
|
||||
zoom-in: Acercar
|
||||
zoom-out: Alejar
|
||||
fullscreen: Pantalla completa
|
||||
exit-fullscreen: Salir de pantalla completa
|
||||
yes: Sí
|
||||
no: No
|
||||
ok: Aceptar
|
||||
none-available: Ninguno disponible
|
||||
default: Predeterminado
|
||||
custom: Personalizado
|
||||
general: General
|
||||
advanced: Avanzado
|
||||
placeholder-text: Introduce texto aquí...
|
||||
learn-more: Aprender más
|
||||
view-resource: Ver {resource}
|
||||
view-resource-details: Ver detalles de {resource}
|
||||
create-a-new-resource: Crear un nuevo {resource}
|
||||
create-new-resource: Crear nuevo {resource}
|
||||
search-resource: Buscar {resource}
|
||||
new-resource: Nuevo {resource}
|
||||
update-resource: Actualizar {resource}
|
||||
save-resource-changes: Guardar cambios de {resource}
|
||||
creating-resource: Creando {resource}
|
||||
cancel-resource: Cancelar {resource}
|
||||
delete-resource: Eliminar {resource}
|
||||
delete-resource-name: 'Eliminar: {resourceName}'
|
||||
delete-resource-named: Eliminar {resource} ({resourceName})
|
||||
delete-resource-prompt: Esta acción no se puede deshacer. Una vez eliminado, el
|
||||
registro será eliminado permanentemente.
|
||||
delete-cannot-be-undone: Esta acción no se puede deshacer. Una vez eliminado, el
|
||||
registro será eliminado permanentemente.
|
||||
create-resource: Crear {resource}
|
||||
edit-resource: Editar {resource}
|
||||
edit-resource-details: Editar detalles de {resource}
|
||||
edit-resource-type-name: 'Editar {resource}: {resourceName}'
|
||||
edit-resource-name: 'Editar: {resourceName}'
|
||||
config: Configuración
|
||||
select-field: Seleccionar {field}
|
||||
columns: Columnas
|
||||
metadata: Metadatos
|
||||
meta: Meta
|
||||
resource-created-success: Nuevo {resource} creado con éxito.
|
||||
resource-created-success-name: Nuevo {resource} ({resourceName}) creado con éxito.
|
||||
resource-updated-success: '{resource} ({resourceName}) actualizado con éxito.'
|
||||
resource-action-success: '{resource} ({resourceName}) {action} con éxito.'
|
||||
resource-deleted-success: '{resource} ({resourceName}) eliminado con éxito.'
|
||||
resource-deleted: '{resource} ({resourceName}) eliminado.'
|
||||
continue-without-saving: ¿Continuar sin guardar?
|
||||
continue-without-saving-prompt: Tienes cambios sin guardar en este {resource}. Continuar
|
||||
descartará esos cambios. Haz clic en Continuar para proceder.
|
||||
|
||||
resource:
|
||||
alert: Alerta
|
||||
alerts: Alertas
|
||||
brand: Marca
|
||||
brands: Marcas
|
||||
category: Categoría
|
||||
categories: Categorías
|
||||
chat-attachment: Archivo adjunto de chat
|
||||
chat-attachments: Archivos adjuntos de chat
|
||||
chat-channel: Canal de chat
|
||||
chat-channels: Canales de chat
|
||||
chat-log: Registro de chat
|
||||
chat-logs: Registros de chat
|
||||
chat-message: Mensaje de chat
|
||||
chat-messages: Mensajes de chat
|
||||
chat-participant: Participante del chat
|
||||
chat-participants: Participantes del chat
|
||||
chat-receipt: Recibo de chat
|
||||
chat-receipts: Recibos de chat
|
||||
comment: Comentario
|
||||
comments: Comentarios
|
||||
company: Empresa
|
||||
companies: Empresas
|
||||
custom-field-value: Valor de campo personalizado
|
||||
custom-field-values: Valores de campo personalizado
|
||||
custom-field: Campo personalizado
|
||||
custom-fields: Campos personalizados
|
||||
dashboard-widget: Widget del panel
|
||||
dashboard-widgets: Widgets del panel
|
||||
dashboard: Panel
|
||||
dashboards: Paneles
|
||||
extension: Extensión
|
||||
extensions: Extensiones
|
||||
file: Archivo
|
||||
files: Archivos
|
||||
group: Grupo
|
||||
groups: Grupos
|
||||
notification: Notificación
|
||||
notifications: Notificaciones
|
||||
permission: Permiso
|
||||
permissions: Permisos
|
||||
policy: Política
|
||||
policies: Políticas
|
||||
report: Informe
|
||||
reports: Informes
|
||||
role: Rol
|
||||
roles: Roles
|
||||
setting: Configuración
|
||||
settings: Configuraciones
|
||||
transaction: Transacción
|
||||
transactions: Transacciones
|
||||
user-device: Dispositivo del usuario
|
||||
user-devices: Dispositivos del usuario
|
||||
user: Usuario
|
||||
users: Usuarios
|
||||
|
||||
dropzone:
|
||||
file: archivo
|
||||
drop-to-upload: Suelta para subir
|
||||
invalid: Inválido
|
||||
files-ready-for-upload: '{numOfFiles} listo(s) para subir.'
|
||||
upload-images-videos: Subir imágenes y videos
|
||||
upload-documents: Subir documentos
|
||||
upload-documents-files: Subir documentos y archivos
|
||||
upload-avatar-files: Subir avatares personalizados
|
||||
dropzone-supported-images-videos: Arrastra y suelta archivos de imagen y video en
|
||||
esta zona
|
||||
dropzone-supported-avatars: Arrastra y suelta archivos SVG o PNG
|
||||
dropzone-supported-files: Arrastra y suelta archivos en esta zona
|
||||
or-select-button-text: o selecciona archivos para subir.
|
||||
upload-queue: Cola de subida
|
||||
uploading: Subiendo...
|
||||
|
||||
two-fa-enforcement-alert:
|
||||
message: Para mejorar la seguridad de tu cuenta, tu organización requiere Autenticación
|
||||
de Dos Factores (2FA). Activa 2FA en la configuración de tu cuenta para una
|
||||
capa adicional de protección.
|
||||
button-text: Configurar 2FA
|
||||
|
||||
comment-thread:
|
||||
publish-comment-button-text: Publicar comentario
|
||||
publish-reply-button-text: Publicar respuesta
|
||||
reply-comment-button-text: Responder
|
||||
edit-comment-button-text: Editar
|
||||
delete-comment-button-text: Eliminar
|
||||
comment-published-ago: hace {createdAgo}
|
||||
comment-input-placeholder: Escribe un nuevo comentario...
|
||||
comment-reply-placeholder: Escribe tu respuesta...
|
||||
comment-input-empty-notification: No puedes publicar comentarios vacíos...
|
||||
comment-min-length-notification: El comentario debe tener al menos 2 caracteres
|
||||
|
||||
dashboard:
|
||||
select-dashboard: Seleccionar Panel
|
||||
create-new-dashboard: Crear nuevo Panel
|
||||
create-a-new-dashboard: Crear un nuevo Panel
|
||||
confirm-create-dashboard: ¡Crear Panel!
|
||||
edit-layout: Editar diseño
|
||||
add-widgets: Agregar widgets
|
||||
delete-dashboard: Eliminar panel
|
||||
save-dashboard: Guardar Panel
|
||||
you-cannot-delete-this-dashboard: No puedes eliminar este panel.
|
||||
are-you-sure-you-want-delete-dashboard: ¿Estás seguro de que quieres eliminar este
|
||||
{dashboardName}?
|
||||
|
||||
dashboard-widget-panel:
|
||||
widget-name: Widget {widgetName}
|
||||
select-widgets: Seleccionar Widgets
|
||||
close-and-save: Cerrar y Guardar
|
||||
|
||||
filters-picker:
|
||||
filters: Filtros
|
||||
filter-data: Filtrar Datos
|
||||
|
||||
visible-column-picker:
|
||||
select-viewable-columns: Seleccionar columnas visibles
|
||||
customize-columns: Personalizar Columnas
|
||||
|
||||
component:
|
||||
file:
|
||||
dropdown-label: Acciones de archivo
|
||||
|
||||
import-modal:
|
||||
loading-message: Procesando importación...
|
||||
drop-upload: Suelta para subir
|
||||
invalid: Inválido
|
||||
ready-upload: listo para subir.
|
||||
upload-spreadsheets: Subir Hojas de Cálculo
|
||||
drag-drop: Arrastra y suelta archivos de hojas de cálculo en esta zona
|
||||
button-text: o selecciona hojas de cálculo para subir
|
||||
spreadsheets: hojas de cálculo
|
||||
upload-queue: Cola de Subida
|
||||
|
||||
dropzone:
|
||||
file: archivo
|
||||
drop-to-upload: Suelta para subir
|
||||
invalid: Inválido
|
||||
files-ready-for-upload: '{numOfFiles} listo(s) para subir.'
|
||||
upload-images-videos: Subir Imágenes y Videos
|
||||
upload-documents: Subir Documentos
|
||||
upload-documents-files: Subir Documentos y Archivos
|
||||
upload-avatar-files: Subir Avatares Personalizados
|
||||
dropzone-supported-images-videos: Arrastra y suelta archivos de imagen y video
|
||||
en esta zona
|
||||
dropzone-supported-avatars: Arrastra y suelta archivos SVG o PNG
|
||||
dropzone-supported-files: Arrastra y suelta archivos en esta zona
|
||||
or-select-button-text: o selecciona archivos para subir.
|
||||
upload-queue: Cola de Subida
|
||||
uploading: Subiendo...
|
||||
|
||||
two-fa-enforcement-alert:
|
||||
message: Para mejorar la seguridad de tu cuenta, tu organización requiere Autenticación
|
||||
de Dos Factores (2FA). Activa 2FA en la configuración de tu cuenta para una
|
||||
capa adicional de protección.
|
||||
button-text: Configurar 2FA
|
||||
|
||||
comment-thread:
|
||||
publish-comment-button-text: Publicar comentario
|
||||
publish-reply-button-text: Publicar respuesta
|
||||
reply-comment-button-text: Responder
|
||||
edit-comment-button-text: Editar
|
||||
delete-comment-button-text: Eliminar
|
||||
comment-published-ago: hace {createdAgo}
|
||||
comment-input-placeholder: Escribe un nuevo comentario...
|
||||
comment-reply-placeholder: Escribe tu respuesta...
|
||||
comment-input-empty-notification: No puedes publicar comentarios vacíos...
|
||||
comment-min-length-notification: El comentario debe tener al menos 2 caracteres
|
||||
|
||||
dashboard:
|
||||
select-dashboard: Seleccionar panel
|
||||
create-new-dashboard: Crear nuevo panel
|
||||
create-a-new-dashboard: Crear un nuevo panel
|
||||
confirm-create-dashboard: ¡Crear panel!
|
||||
edit-layout: Editar diseño
|
||||
add-widgets: Agregar widgets
|
||||
delete-dashboard: Eliminar panel
|
||||
save-dashboard: Guardar panel
|
||||
you-cannot-delete-this-dashboard: No puedes eliminar este panel.
|
||||
are-you-sure-you-want-delete-dashboard: ¿Estás seguro de que quieres eliminar
|
||||
{dashboardName}?
|
||||
|
||||
dashboard-widget-panel:
|
||||
widget-name: Widget {widgetName}
|
||||
select-widgets: Seleccionar widgets
|
||||
close-and-save: Cerrar y guardar
|
||||
|
||||
services:
|
||||
dashboard-service:
|
||||
create-dashboard-success-notification: Nuevo panel `{dashboardName}` creado con
|
||||
éxito.
|
||||
delete-dashboard-success-notification: El panel `{dashboardName}` fue eliminado.
|
||||
|
||||
auth:
|
||||
verification:
|
||||
header-title: Verificación de cuenta
|
||||
title: Verifica tu dirección de correo electrónico
|
||||
message-text: <strong>¡Casi terminado!</strong><br> Revisa tu correo para un código
|
||||
de verificación.
|
||||
verification-code-text: Introduce el código de verificación que recibiste por
|
||||
correo.
|
||||
verification-input-label: Código de verificación
|
||||
verify-button-text: Verificar y continuar
|
||||
didnt-receive-a-code: ¿No has recibido un código?
|
||||
not-sent:
|
||||
message: ¿No has recibido un código?
|
||||
alternative-choice: Usa las opciones alternativas abajo para verificar tu cuenta.
|
||||
resend-email: Reenviar correo
|
||||
send-by-sms: Enviar por SMS
|
||||
|
||||
two-fa:
|
||||
verify-code:
|
||||
verification-code: Código de verificación
|
||||
check-title: Revisa tu correo o teléfono
|
||||
check-subtitle: Te hemos enviado un código de verificación. Introduce el código
|
||||
abajo para completar el proceso de inicio de sesión.
|
||||
expired-help-text: Tu código de autenticación 2FA ha expirado. Puedes solicitar
|
||||
otro código si necesitas más tiempo.
|
||||
resend-code: Reenviar código
|
||||
verify-code: Verificar código
|
||||
cancel-two-factor: Cancelar autenticación de dos factores
|
||||
invalid-session-error-notification: Sesión inválida. Por favor, inténtalo de
|
||||
nuevo.
|
||||
verification-successful-notification: ¡Verificación exitosa!
|
||||
verification-code-expired-notification: El código de verificación ha expirado.
|
||||
Por favor, solicita uno nuevo.
|
||||
verification-code-failed-notification: La verificación falló. Por favor, inténtalo
|
||||
de nuevo.
|
||||
|
||||
resend-code:
|
||||
verification-code-resent-notification: Nuevo código de verificación enviado.
|
||||
verification-code-resent-error-notification: Error al reenviar el código de
|
||||
verificación. Por favor, inténtalo de nuevo.
|
||||
|
||||
forgot-password:
|
||||
success-message: ¡Revisa tu correo para continuar!
|
||||
is-sent:
|
||||
title: ¡Casi listo!
|
||||
message: <strong>¡Revisa tu correo electrónico!</strong><br> Te hemos enviado
|
||||
un enlace mágico a tu correo que te permitirá restablecer tu contraseña. El
|
||||
enlace expira en 15 minutos.
|
||||
not-sent:
|
||||
title: ¿Olvidaste tu contraseña?
|
||||
message: <strong>No te preocupes, te respaldamos.</strong><br> Introduce el
|
||||
correo electrónico que usas para iniciar sesión en {appName} y te enviaremos
|
||||
un enlace seguro para restablecer tu contraseña.
|
||||
form:
|
||||
email-label: Tu dirección de correo electrónico
|
||||
submit-button: ¡OK, envíame un enlace mágico!
|
||||
nevermind-button: No importa
|
||||
|
||||
login:
|
||||
title: Inicia sesión en tu cuenta
|
||||
no-identity-notification: ¿Olvidaste ingresar tu correo electrónico?
|
||||
no-password-notification: ¿Olvidaste ingresar tu contraseña?
|
||||
unverified-notification: Tu cuenta necesita ser verificada para continuar.
|
||||
password-reset-required: Se requiere un restablecimiento de contraseña para continuar.
|
||||
failed-attempt:
|
||||
message: <strong>¿Olvidaste tu contraseña?</strong><br> Haz clic en el botón
|
||||
de abajo para restablecer tu contraseña.
|
||||
button-text: ¡Ok, ayúdame a restablecerla!
|
||||
|
||||
form:
|
||||
email-label: Correo electrónico
|
||||
password-label: Contraseña
|
||||
remember-me-label: Recuérdame
|
||||
forgot-password-label: ¿Olvidaste tu contraseña?
|
||||
sign-in-button: Iniciar sesión
|
||||
create-account-button: Crear una nueva cuenta
|
||||
slow-connection-message: Experimentando problemas de conectividad.
|
||||
|
||||
reset-password:
|
||||
success-message: ¡Tu contraseña ha sido restablecida! Inicia sesión para continuar.
|
||||
invalid-verification-code: Este enlace para restablecer la contraseña es inválido
|
||||
o ha expirado.
|
||||
title: Restablece tu contraseña
|
||||
form:
|
||||
code:
|
||||
label: Tu código de restablecimiento
|
||||
help-text: El código de verificación que recibiste en tu correo electrónico.
|
||||
password:
|
||||
label: Nueva contraseña
|
||||
help-text: Introduce una contraseña de al menos 6 caracteres para continuar.
|
||||
confirm-password:
|
||||
label: Confirma la nueva contraseña
|
||||
help-text: Introduce una contraseña de al menos 6 caracteres para continuar.
|
||||
submit-button: Restablecer contraseña
|
||||
back-button: Volver
|
||||
|
||||
console:
|
||||
create-or-join-organization:
|
||||
modal-title: Crear o unirse a una organización
|
||||
join-success-notification: ¡Te has unido a una nueva organización!
|
||||
create-success-notification: ¡Has creado una nueva organización!
|
||||
|
||||
switch-organization:
|
||||
modal-title: ¿Estás seguro de que quieres cambiar la organización a {organizationName}?
|
||||
modal-body: Al confirmar, tu cuenta permanecerá iniciada, pero tu organización
|
||||
principal será cambiada.
|
||||
modal-accept-button-text: Sí, quiero cambiar de organización
|
||||
success-notification: Has cambiado de organización
|
||||
|
||||
account:
|
||||
index:
|
||||
upload-new: Subir nuevo
|
||||
phone: Tu número de teléfono.
|
||||
photos: fotos
|
||||
timezone: Selecciona tu zona horaria.
|
||||
|
||||
admin:
|
||||
menu:
|
||||
overview: Resumen
|
||||
organizations: Organizaciones
|
||||
branding: Marca
|
||||
2fa-config: Configuración 2FA
|
||||
schedule-monitor: Monitor de programación
|
||||
services: Servicios
|
||||
mail: Correo
|
||||
filesystem: Sistema de archivos
|
||||
queue: Cola
|
||||
socket: Socket
|
||||
push-notifications: Notificaciones push
|
||||
|
||||
schedule-monitor:
|
||||
schedule-monitor: Monitor de programación
|
||||
task-logs-for: 'Registros de tareas para: '
|
||||
showing-last-count: Mostrando los últimos {count} registros
|
||||
name: Nombre
|
||||
type: Tipo
|
||||
timezone: Zona horaria
|
||||
last-started: Último inicio
|
||||
last-finished: Última finalización
|
||||
last-failure: Último fallo
|
||||
date: Fecha
|
||||
memory: Memoria
|
||||
runtime: Tiempo de ejecución
|
||||
output: Salida
|
||||
no-output: Sin salida
|
||||
|
||||
config:
|
||||
database:
|
||||
title: Configuración de la base de datos
|
||||
filesystem:
|
||||
title: Configuración del sistema de archivos
|
||||
mail:
|
||||
title: Configuración de correo
|
||||
notification-channels:
|
||||
title: Configuración de notificaciones push
|
||||
queue:
|
||||
title: Configuración de la cola
|
||||
services:
|
||||
title: Configuración de servicios
|
||||
socket:
|
||||
title: Configuración de socket
|
||||
|
||||
branding:
|
||||
title: Marca
|
||||
icon-text: Ícono
|
||||
upload-new: Subir nuevo
|
||||
reset-default: Restablecer a predeterminado
|
||||
logo-text: Logo
|
||||
theme: Tema predeterminado
|
||||
|
||||
index:
|
||||
total-users: Total de usuarios
|
||||
total-organizations: Total de organizaciones
|
||||
total-transactions: Total de transacciones
|
||||
|
||||
notifications:
|
||||
title: Notificaciones
|
||||
notification-settings: Configuración de notificaciones
|
||||
|
||||
organizations:
|
||||
index:
|
||||
title: Organizaciones
|
||||
owner-name-column: Propietario
|
||||
owner-phone-column: Teléfono del propietario
|
||||
owner-email-column: Correo electrónico del propietario
|
||||
users-count-column: Usuarios
|
||||
phone-column: Teléfono
|
||||
email-column: Correo electrónico
|
||||
users:
|
||||
title: Usuarios
|
||||
|
||||
settings:
|
||||
index:
|
||||
title: Configuración de la organización
|
||||
organization-name: Nombre de la organización
|
||||
organization-description: Descripción de la organización
|
||||
organization-phone: Número de teléfono de la organización
|
||||
organization-currency: Moneda de la organización
|
||||
organization-id: ID de la organización
|
||||
organization-branding: Marca de la organización
|
||||
logo: Logo
|
||||
logo-help-text: Logo para tu organización.
|
||||
upload-new-logo: Subir nuevo logo
|
||||
backdrop: Fondo
|
||||
backdrop-help-text: Banner o imagen de fondo opcional para tu organización.
|
||||
upload-new-backdrop: Subir nuevo fondo
|
||||
organization-timezone: Selecciona la zona horaria predeterminada para tu organización.
|
||||
select-timezone: Selecciona zona horaria.
|
||||
|
||||
extensions:
|
||||
title: ¡Las extensiones llegarán pronto!
|
||||
message: Por favor, vuelve a consultar en las próximas versiones mientras preparamos
|
||||
el lanzamiento del repositorio y mercado de Extensiones.
|
||||
|
||||
notifications:
|
||||
select-all: Seleccionar todo
|
||||
mark-as-read: Marcar como leído
|
||||
received: 'Recibido:'
|
||||
message: No hay notificaciones para mostrar.
|
||||
|
||||
invite:
|
||||
for-users:
|
||||
invitation-message: Has sido invitado a unirte a {companyName}
|
||||
invitation-sent-message: Has sido invitado a unirte a la organización {companyName}
|
||||
en {appName}. Para aceptar esta invitación, introduce tu código de invitación
|
||||
recibido por correo electrónico y haz clic en continuar.
|
||||
invitation-code-sent-text: Tu código de invitación
|
||||
accept-invitation-text: Aceptar invitación
|
||||
|
||||
onboard:
|
||||
index:
|
||||
title: Crea tu cuenta
|
||||
welcome-title: <strong>¡Bienvenido a {companyName}!</strong><br />
|
||||
welcome-text: Completa los detalles requeridos a continuación para comenzar.
|
||||
full-name: Nombre completo
|
||||
full-name-help-text: Tu nombre completo
|
||||
your-email: Dirección de correo electrónico
|
||||
your-email-help-text: Tu dirección de correo electrónico
|
||||
phone: Número de teléfono
|
||||
phone-help-text: Tu número de teléfono
|
||||
organization-name: Nombre de la organización
|
||||
organization-help-text: El nombre de tu organización, todos tus servicios y recursos
|
||||
serán gestionados bajo esta organización, más adelante podrás crear tantas organizaciones
|
||||
como quieras o necesites.
|
||||
password: Introduce una contraseña
|
||||
password-help-text: Tu contraseña, asegúrate de que sea buena.
|
||||
confirm-password: Confirma tu contraseña
|
||||
confirm-password-help-text: Solo para confirmar la contraseña que introdujiste
|
||||
arriba.
|
||||
continue-button-text: Continuar
|
||||
|
||||
verify-email:
|
||||
header-title: Verificación de cuenta
|
||||
title: Verifica tu dirección de correo electrónico
|
||||
message-text: <strong>¡Casi terminado!</strong><br> Revisa tu correo electrónico
|
||||
para un código de verificación.
|
||||
verification-code-text: Introduce el código de verificación que recibiste por
|
||||
correo electrónico.
|
||||
verification-input-label: Código de verificación
|
||||
verify-button-text: Verificar y continuar
|
||||
didnt-receive-a-code: ¿No has recibido un código todavía?
|
||||
not-sent:
|
||||
message: ¿No has recibido un código todavía?
|
||||
alternative-choice: Usa las opciones alternativas a continuación para verificar
|
||||
tu cuenta.
|
||||
resend-email: Reenviar correo electrónico
|
||||
send-by-sms: Enviar por SMS
|
||||
|
||||
install:
|
||||
installer-header: Instalador
|
||||
failed-message-sent: ¡La instalación falló! Haz clic en el botón de abajo para reintentar
|
||||
la instalación.
|
||||
retry-install: Reintentar instalación
|
||||
start-install: Iniciar instalación
|
||||
|
||||
layout:
|
||||
header:
|
||||
menus:
|
||||
organization:
|
||||
settings: Configuración de la organización
|
||||
create-or-join: Crear o unirse a organizaciones
|
||||
explore-extensions: Explorar extensiones
|
||||
user:
|
||||
view-profile: Ver perfil
|
||||
keyboard-shortcuts: Mostrar atajos de teclado
|
||||
changelog: Registro de cambios
|
||||
|
||||
741
console/translations/fa-ir.yaml
Normal file
741
console/translations/fa-ir.yaml
Normal file
@@ -0,0 +1,741 @@
|
||||
app:
|
||||
name: فلیتبیس
|
||||
common:
|
||||
new: جدید
|
||||
create: ایجاد
|
||||
add: افزودن
|
||||
edit: ویرایش
|
||||
update: بهروزرسانی
|
||||
save: ذخیره
|
||||
save-changes: ذخیره تغییرات
|
||||
delete: حذف
|
||||
delete-selected: حذف انتخابشدهها
|
||||
delete-selected-count: حذف {count} مورد انتخابشده
|
||||
your-profile: پروفایل شما
|
||||
date-of-birth: تاریخ تولد
|
||||
organization: سازمان
|
||||
two-factor: احراز هویت دو مرحلهای
|
||||
remove: حذف
|
||||
cancel: لغو
|
||||
confirm: تأیید
|
||||
close: بستن
|
||||
open: باز کردن
|
||||
view: مشاهده
|
||||
preview: پیشنمایش
|
||||
upload: بارگذاری
|
||||
download: دانلود
|
||||
import: وارد کردن
|
||||
export: صادر کردن
|
||||
print: چاپ
|
||||
duplicate: تکثیر
|
||||
copy: کپی
|
||||
paste: جایگذاری
|
||||
share: اشتراکگذاری
|
||||
refresh: تازهسازی
|
||||
reset: بازنشانی
|
||||
retry: تلاش مجدد
|
||||
back: بازگشت
|
||||
next: بعدی
|
||||
previous: قبلی
|
||||
submit: ارسال
|
||||
apply: اعمال
|
||||
continue: ادامه
|
||||
proceed: ادامه دادن
|
||||
select: انتخاب
|
||||
deselect: لغو انتخاب
|
||||
search: جستجو
|
||||
filter: فیلتر
|
||||
sort: مرتبسازی
|
||||
view-all: مشاهده همه
|
||||
clear: پاک کردن
|
||||
done: انجام شد
|
||||
finish: پایان
|
||||
skip: رد کردن
|
||||
method: روش
|
||||
bulk-delete: حذف گروهی
|
||||
bulk-delete-resource: حذف گروهی {resource}
|
||||
bulk-cancel: لغو گروهی
|
||||
bulk-cancel-resource: لغو گروهی {resource}
|
||||
bulk-actions: اقدامات گروهی
|
||||
column: ستون
|
||||
row: ردیف
|
||||
table: جدول
|
||||
list: لیست
|
||||
grid: شبکه
|
||||
form: فرم
|
||||
field: فیلد
|
||||
section: بخش
|
||||
panel: پنل
|
||||
card: کارت
|
||||
tab: زبانه
|
||||
modal: مودال
|
||||
dialog: دیالوگ
|
||||
menu: منو
|
||||
dropdown: کشویی
|
||||
tooltip: راهنمای ابزار
|
||||
sidebar: نوار کناری
|
||||
toolbar: نوار ابزار
|
||||
footer: پاورقی
|
||||
header: سربرگ
|
||||
title: عنوان
|
||||
subtitle: زیرعنوان
|
||||
description: توضیحات
|
||||
placeholder: مکاننما
|
||||
label: برچسب
|
||||
button: دکمه
|
||||
icon: آیکون
|
||||
avatar: آواتار
|
||||
link: لینک
|
||||
badge: نشان
|
||||
tag: برچسب
|
||||
banner: بنر
|
||||
step: مرحله
|
||||
progress: پیشرفت
|
||||
map: نقشه
|
||||
board: تابلو
|
||||
loading: در حال بارگذاری
|
||||
loading-resource: در حال بارگذاری {resource}
|
||||
saving: در حال ذخیره
|
||||
processing: در حال پردازش
|
||||
fetching: در حال دریافت
|
||||
updating: در حال بهروزرسانی
|
||||
uploading: در حال بارگذاری
|
||||
completed: تکمیل شد
|
||||
success: موفقیت
|
||||
failed: ناموفق
|
||||
error: خطا
|
||||
warning: هشدار
|
||||
info: اطلاعات
|
||||
ready: آماده
|
||||
activity: فعالیت
|
||||
active: فعال
|
||||
inactive: غیرفعال
|
||||
enabled: فعالشده
|
||||
disabled: غیرفعالشده
|
||||
online: آنلاین
|
||||
offline: آفلاین
|
||||
pending: در انتظار
|
||||
archived: بایگانیشده
|
||||
hidden: مخفی
|
||||
visible: قابل مشاهده
|
||||
empty: خالی
|
||||
not-found: پیدا نشد
|
||||
no-results: بدون نتیجه
|
||||
try-again: دوباره امتحان کنید
|
||||
are-you-sure: آیا مطمئن هستید؟
|
||||
changes-saved: تغییرات با موفقیت ذخیره شد
|
||||
saved-successfully: تغییرات با موفقیت ذخیره شد
|
||||
field-saved: "{field} با موفقیت ذخیره شد"
|
||||
changes-discarded: تغییرات لغو شد
|
||||
delete-confirm: آیا مطمئن هستید که میخواهید این مورد را حذف کنید؟
|
||||
action-successful: اقدام با موفقیت انجام شد
|
||||
action-failed: اقدام ناموفق بود. لطفاً دوباره امتحان کنید
|
||||
something-went-wrong: مشکلی پیش آمد
|
||||
please-wait: لطفاً منتظر بمانید...
|
||||
sign-in: ورود
|
||||
sign-out: خروج
|
||||
sign-up: ثبتنام
|
||||
log-in: ورود
|
||||
log-out: خروج
|
||||
register: ثبت
|
||||
forgot-password: رمز عبور را فراموش کردهاید؟
|
||||
reset-password: بازنشانی رمز عبور
|
||||
change-password: تغییر رمز عبور
|
||||
password: رمز عبور
|
||||
confirm-password: تأیید رمز عبور
|
||||
email: ایمیل
|
||||
username: نام کاربری
|
||||
remember-me: مرا به خاطر بسپار
|
||||
welcome: خوش آمدید
|
||||
welcome-back: خوش آمدید دوباره
|
||||
profile: پروفایل
|
||||
account: حساب کاربری
|
||||
settings: تنظیمات
|
||||
preferences: ترجیحات
|
||||
record: رکورد
|
||||
records: رکوردها
|
||||
item: مورد
|
||||
items: موارد
|
||||
entry: ورودی
|
||||
entries: ورودیها
|
||||
id: شناسه
|
||||
name: نام
|
||||
type: نوع
|
||||
category: دستهبندی
|
||||
overview: مرور
|
||||
value: مقدار
|
||||
amount: مبلغ
|
||||
price: قیمت
|
||||
quantity: تعداد
|
||||
status: وضعیت
|
||||
date: تاریخ
|
||||
date-created: تاریخ ایجاد
|
||||
date-updated: تاریخ بهروزرسانی
|
||||
time: زمان
|
||||
created-at: ایجاد شده در
|
||||
updated-at: بهروزرسانی شده در
|
||||
expired-at: منقضی شده در
|
||||
last-seen-at: آخرین بازدید در
|
||||
last-modified: آخرین ویرایش
|
||||
last-modified-data: "آخرین ویرایش: {date}"
|
||||
actions: اقدامات
|
||||
details: جزئیات
|
||||
notes: یادداشتها
|
||||
reference: مرجع
|
||||
filter-by: فیلتر بر اساس
|
||||
filter-by-field: فیلتر بر اساس {field}
|
||||
sort-by: مرتبسازی بر اساس
|
||||
ascending: صعودی
|
||||
descending: نزولی
|
||||
all: همه
|
||||
none: هیچکدام
|
||||
select-all: انتخاب همه
|
||||
deselect-all: لغو انتخاب همه
|
||||
show-more: نمایش بیشتر
|
||||
show-less: نمایش کمتر
|
||||
page: صفحه
|
||||
of: از
|
||||
total: مجموع
|
||||
items-per-page: موارد در هر صفحه
|
||||
showing: در حال نمایش
|
||||
to: تا
|
||||
results: نتایج
|
||||
load-more: بارگذاری بیشتر
|
||||
no-more-results: نتیجه دیگری وجود ندارد
|
||||
today: امروز
|
||||
yesterday: دیروز
|
||||
tomorrow: فردا
|
||||
day: روز
|
||||
week: هفته
|
||||
month: ماه
|
||||
year: سال
|
||||
date-range: بازه زمانی
|
||||
start-date: تاریخ شروع
|
||||
end-date: تاریخ پایان
|
||||
time-zone: منطقه زمانی
|
||||
system: سیستم
|
||||
dashboard: داشبورد
|
||||
home: خانه
|
||||
analytics: تحلیلها
|
||||
reports: گزارشها
|
||||
logs: لاگها
|
||||
help: کمک
|
||||
support: پشتیبانی
|
||||
contact: تماس
|
||||
documentation: مستندات
|
||||
language: زبان
|
||||
timezone: منطقه زمانی
|
||||
version: نسخه
|
||||
theme: تم
|
||||
light-mode: حالت روشن
|
||||
dark-mode: حالت تیره
|
||||
update-available: بهروزرسانی در دسترس است
|
||||
install-update: نصب بهروزرسانی
|
||||
maintenance-mode: حالت نگهداری
|
||||
notification: اعلان
|
||||
notifications: اعلانها
|
||||
mark-as-read: علامتگذاری به عنوان خواندهشده
|
||||
mark-all-as-read: علامتگذاری همه به عنوان خواندهشده
|
||||
clear-notifications: پاک کردن اعلانها
|
||||
company: شرکت
|
||||
companies: شرکتها
|
||||
user: کاربر
|
||||
users: کاربران
|
||||
role: نقش
|
||||
roles: نقشها
|
||||
permission: مجوز
|
||||
permissions: مجوزها
|
||||
group: گروه
|
||||
groups: گروهها
|
||||
unauthorized: غیرمجاز
|
||||
forbidden: ممنوع
|
||||
resource-not-found: منبع پیدا نشد
|
||||
server-error: خطای سرور
|
||||
validation-error: خطای اعتبارسنجی
|
||||
timeout-error: درخواست منقضی شد
|
||||
network-error: خطای شبکه
|
||||
unknown-error: خطای ناشناخته
|
||||
file: فایل
|
||||
files: فایلها
|
||||
folder: پوشه
|
||||
folders: پوشهها
|
||||
upload-file: بارگذاری فایل
|
||||
upload-files: بارگذاری فایلها
|
||||
upload-image: بارگذاری تصویر
|
||||
upload-image-supported: پشتیبانی از PNG، JPEG و GIF
|
||||
choose-file: انتخاب فایل
|
||||
choose-files: انتخاب فایلها
|
||||
drag-and-drop: کشیدن و رها کردن
|
||||
download-file: دانلود فایل
|
||||
file-size: اندازه فایل
|
||||
file-type: نوع فایل
|
||||
confirm-delete: تأیید حذف
|
||||
confirm-action: تأیید اقدام
|
||||
confirm-exit: تأیید خروج
|
||||
confirm-and-save-changes: تأیید و ذخیره تغییرات
|
||||
are-you-sure-exit: آیا مطمئن هستید که میخواهید خارج شوید؟
|
||||
unsaved-changes-warning: شما تغییرات ذخیرهنشده دارید
|
||||
connected: متصل
|
||||
disconnected: قطعشده
|
||||
reconnecting: در حال اتصال مجدد
|
||||
connection-lost: اتصال قطع شد
|
||||
connection-restored: اتصال برقرار شد
|
||||
show: نمایش
|
||||
hide: مخفی کردن
|
||||
expand: گسترش
|
||||
collapse: جمع کردن
|
||||
enable: فعال کردن
|
||||
disable: غیرفعال کردن
|
||||
minimize: کوچک کردن
|
||||
maximize: بزرگ کردن
|
||||
restore: بازیابی
|
||||
zoom-in: بزرگنمایی
|
||||
zoom-out: کوچکنمایی
|
||||
fullscreen: تمام صفحه
|
||||
exit-fullscreen: خروج از حالت تمام صفحه
|
||||
yes: بله
|
||||
no: خیر
|
||||
ok: تأیید
|
||||
none-available: هیچکدام در دسترس نیست
|
||||
default: پیشفرض
|
||||
custom: سفارشی
|
||||
general: عمومی
|
||||
advanced: پیشرفته
|
||||
placeholder-text: اینجا متن وارد کنید...
|
||||
learn-more: اطلاعات بیشتر
|
||||
view-resource: مشاهده {resource}
|
||||
view-resource-details: مشاهده جزئیات {resource}
|
||||
create-a-new-resource: ایجاد یک {resource} جدید
|
||||
create-new-resource: ایجاد {resource} جدید
|
||||
search-resource: جستجوی {resource}
|
||||
new-resource: "{resource} جدید"
|
||||
update-resource: بهروزرسانی {resource}
|
||||
save-resource-changes: ذخیره تغییرات {resource}
|
||||
creating-resource: در حال ایجاد {resource}
|
||||
cancel-resource: لغو {resource}
|
||||
delete-resource: حذف {resource}
|
||||
delete-resource-name: "حذف: {resourceName}"
|
||||
delete-resource-named: حذف {resource} ({resourceName})
|
||||
delete-resource-prompt: این اقدام قابل بازگشت نیست. پس از حذف، رکورد برای همیشه حذف خواهد شد
|
||||
delete-cannot-be-undone: این اقدام قابل بازگشت نیست. پس از حذف، رکورد برای همیشه حذف خواهد شد
|
||||
create-resource: ایجاد {resource}
|
||||
edit-resource: ویرایش {resource}
|
||||
edit-resource-details: ویرایش جزئیات {resource}
|
||||
edit-resource-type-name: "ویرایش {resource}: {resourceName}"
|
||||
edit-resource-name: "ویرایش: {resourceName}"
|
||||
config: پیکربندی
|
||||
select-field: انتخاب {field}
|
||||
columns: ستونها
|
||||
metadata: متادیتا
|
||||
meta: متا
|
||||
resource-created-success: "{resource} جدید با موفقیت ایجاد شد"
|
||||
resource-created-success-name: "{resource} جدید important! ({resourceName}) با موفقیت ایجاد شد"
|
||||
resource-updated-success: "{resource} ({resourceName}) با موفقیت بهروزرسانی شد"
|
||||
resource-action-success: "{resource} ({resourceName}) با موفقیت {action} شد"
|
||||
resource-deleted-success: "{resource} ({resourceName}) با موفقیت حذف شد"
|
||||
resource-deleted: "{resource} ({resourceName}) حذف شد"
|
||||
continue-without-saving: ادامه بدون ذخیره؟
|
||||
continue-without-saving-prompt: شما تغییرات ذخیرهنشدهای در این {resource} دارید. ادامه دادن باعث لغو آنها خواهد شد. برای ادامه کلیک کنید
|
||||
resource:
|
||||
alert: هشدار
|
||||
alerts: هشدارها
|
||||
brand: برند
|
||||
brands: برندها
|
||||
category: دستهبندی
|
||||
categories: دستهبندیها
|
||||
chat-attachment: پیوست چت
|
||||
chat-attachments: پیوستهای چت
|
||||
chat-channel: کانال چت
|
||||
chat-channels: کانالهای چت
|
||||
chat-log: لاگ چت
|
||||
chat-logs: لاگهای چت
|
||||
chat-message: پیام چت
|
||||
chat-messages: پیامهای چت
|
||||
chat-participant: شرکتکننده چت
|
||||
chat-participants: شرکتکنندگان چت
|
||||
chat-receipt: رسید چت
|
||||
chat-receipts: رسیدهای چت
|
||||
comment: نظر
|
||||
comments: نظرات
|
||||
company: شرکت
|
||||
companies: شرکتها
|
||||
custom-field-value: مقدار فیلد سفارشی
|
||||
custom-field-values: مقادیر فیلد سفارشی
|
||||
custom-field: فیلد سفارشی
|
||||
custom-fields: فیلدهای سفارشی
|
||||
dashboard-widget: ویجت داشبورد
|
||||
dashboard-widgets: ویجتهای داشبورد
|
||||
dashboard: داشبورد
|
||||
dashboards: داشبوردها
|
||||
extension: افزونه
|
||||
extensions: افزونهها
|
||||
file: فایل
|
||||
files: فایلها
|
||||
group: گروه
|
||||
groups: گروهها
|
||||
notification: اعلان
|
||||
notifications: اعلانها
|
||||
permission: مجوز
|
||||
permissions: مجوزها
|
||||
policy: سیاست
|
||||
policies: سیاستها
|
||||
report: گزارش
|
||||
reports: گزارشها
|
||||
role: نقش
|
||||
roles: نقشها
|
||||
setting: تنظیم
|
||||
settings: تنظیمات
|
||||
transaction: تراکنش
|
||||
transactions: تراکنشها
|
||||
user-device: دستگاه کاربر
|
||||
user-devices: دستگاههای کاربر
|
||||
user: کاربر
|
||||
users: کاربران
|
||||
dropzone:
|
||||
file: فایل
|
||||
drop-to-upload: برای بارگذاری رها کنید
|
||||
invalid: نامعتبر
|
||||
files-ready-for-upload: "{numOfFiles} آماده برای بارگذاری"
|
||||
upload-images-videos: بارگذاری تصاویر و ویدئوها
|
||||
upload-documents: بارگذاری اسناد
|
||||
upload-documents-files: بارگذاری اسناد و فایلها
|
||||
upload-avatar-files: بارگذاری آواتارهای سفارشی
|
||||
dropzone-supported-images-videos: فایلهای تصویر و ویدئو را به این منطقه رها کنید
|
||||
dropzone-supported-avatars: فایلهای SVG یا PNG را رها کنید
|
||||
dropzone-supported-files: فایلها را به این منطقه رها کنید
|
||||
or-select-button-text: یا فایلها را برای بارگذاری انتخاب کنید
|
||||
upload-queue: صف بارگذاری
|
||||
uploading: در حال بارگذاری...
|
||||
two-fa-enforcement-alert:
|
||||
message: برای افزایش امنیت حساب کاربری خود، سازمان شما احراز هویت دو مرحلهای (2FA) را الزامی کرده است. 2FA را در تنظیمات حساب خود فعال کنید تا لایه امنیتی بیشتری داشته باشید
|
||||
button-text: تنظیم 2FA
|
||||
comment-thread:
|
||||
publish-comment-button-text: انتشار نظر
|
||||
publish-reply-button-text: انتشار پاسخ
|
||||
reply-comment-button-text: پاسخ
|
||||
edit-comment-button-text: ویرایش
|
||||
delete-comment-button-text: حذف
|
||||
comment-published-ago: "{createdAgo} پیش"
|
||||
comment-input-placeholder: یک نظر جدید وارد کنید...
|
||||
comment-reply-placeholder: پاسخ خود را وارد کنید...
|
||||
comment-input-empty-notification: نمیتوانید نظرات خالی منتشر کنید...
|
||||
comment-min-length-notification: نظر باید حداقل 2 کاراکتر باشد
|
||||
dashboard:
|
||||
select-dashboard: انتخاب داشبورد
|
||||
create-new-dashboard: ایجاد داشبورد جدید
|
||||
create-a-new-dashboard: ایجاد یک داشبورد جدید
|
||||
confirm-create-dashboard: ایجاد داشبورد!
|
||||
edit-layout: ویرایش چیدمان
|
||||
add-widgets: افزودن ویجتها
|
||||
delete-dashboard: حذف داشبورد
|
||||
save-dashboard: ذخیره داشبورد
|
||||
you-cannot-delete-this-dashboard: نمیتوانید این داشبورد را حذف کنید
|
||||
are-you-sure-you-want-delete-dashboard: آیا مطمئن هستید که میخواهید {dashboardName} را حذف کنید؟
|
||||
dashboard-widget-panel:
|
||||
widget-name: "ویجت {widgetName}"
|
||||
select-widgets: انتخاب ویجتها
|
||||
close-and-save: بستن و ذخیره
|
||||
filters-picker:
|
||||
filters: فیلترها
|
||||
filter-data: فیلتر دادهها
|
||||
visible-column-picker:
|
||||
select-viewable-columns: انتخاب ستونهای قابل مشاهده
|
||||
customize-columns: سفارشیسازی ستونها
|
||||
component:
|
||||
file:
|
||||
dropdown-label: اقدامات فایل
|
||||
import-modal:
|
||||
loading-message: در حال پردازش وارد کردن...
|
||||
drop-upload: برای بارگذاری رها کنید
|
||||
invalid: نامعتبر
|
||||
ready-upload: آماده برای بارگذاری
|
||||
upload-spreadsheets: بارگذاری صفحات گسترده
|
||||
drag-drop: فایلهای صفحه گسترده را به این منطقه رها کنید
|
||||
button-text: یا صفحات گسترده را برای بارگذاری انتخاب کنید
|
||||
spreadsheets: صفحات گسترده
|
||||
upload-queue: صف بارگذاری
|
||||
dropzone:
|
||||
file: فایل
|
||||
drop-to-upload: برای بارگذاری رها کنید
|
||||
invalid: نامعتبر
|
||||
files-ready-for-upload: "{numOfFiles} آماده برای بارگذاری"
|
||||
upload-images-videos: بارگذاری تصاویر و ویدئوها
|
||||
upload-documents: بارگذاری اسناد
|
||||
upload-documents-files: بارگذاری اسناد و فایلها
|
||||
upload-avatar-files: بارگذاری آواتارهای سفارشی
|
||||
dropzone-supported-images-videos: فایلهای تصویر و ویدئو را به این منطقه رها کنید
|
||||
dropzone-supported-avatars: فایلهای SVG یا PNG را رها کنید
|
||||
dropzone-supported-files: فایلها را به این منطقه رها کنید
|
||||
or-select-button-text: یا فایلها را برای بارگذاری انتخاب کنید
|
||||
upload-queue: صف بارگذاری
|
||||
uploading: در حال بارگذاری...
|
||||
two-fa-enforcement-alert:
|
||||
message: برای افزایش امنیت حساب کاربری خود، سازمان شما احراز هویت دو مرحلهای (2FA) را الزامی کرده است. 2FA را در تنظیمات حساب خود فعال کنید تا لایه امنیتی بیشتری داشته باشید
|
||||
button-text: تنظیم 2FA
|
||||
comment-thread:
|
||||
publish-comment-button-text: انتشار نظر
|
||||
publish-reply-button-text: انتشار پاسخ
|
||||
reply-comment-button-text: پاسخ
|
||||
edit-comment-button-text: ویرایش
|
||||
delete-comment-button-text: حذف
|
||||
comment-published-ago: "{createdAgo} پیش"
|
||||
comment-input-placeholder: یک نظر جدید وارد کنید...
|
||||
comment-reply-placeholder: پاسخ خود را وارد کنید...
|
||||
comment-input-empty-notification: نمیتوانید نظرات خالی منتشر کنید...
|
||||
comment-min-length-notification: نظر باید حداقل 2 کاراکتر باشد
|
||||
dashboard:
|
||||
select-dashboard: انتخاب داشبورد
|
||||
create-new-dashboard: ایجاد داشبورد جدید
|
||||
create-a-new-dashboard: ایجاد یک داشبورد جدید
|
||||
confirm-create-dashboard: ایجاد داشبورد!
|
||||
edit-layout: ویرایش چیدمان
|
||||
add-widgets: افزودن ویجتها
|
||||
delete-dashboard: حذف داشبورد
|
||||
save-dashboard: ذخیره داشبورد
|
||||
you-cannot-delete-this-dashboard: نمیتوانید این داشبورد را حذف کنید
|
||||
are-you-sure-you-want-delete-dashboard: آیا مطمئن هستید که میخواهید {dashboardName} را حذف کنید؟
|
||||
dashboard-widget-panel:
|
||||
widget-name: "ویجت {widgetName}"
|
||||
select-widgets: انتخاب ویجتها
|
||||
close-and-save: بستن و ذخیره
|
||||
services:
|
||||
dashboard-service:
|
||||
create-dashboard-success-notification: داشبورد جدید `{dashboardName}` با موفقیت ایجاد شد
|
||||
delete-dashboard-success-notification: داشبورد `{dashboardName}` حذف شد
|
||||
auth:
|
||||
verification:
|
||||
header-title: تأیید حساب
|
||||
title: ایمیل خود را تأیید کنید
|
||||
message-text: "<strong>تقریباً تمام شد!</strong><br> ایمیل خود را برای دریافت کد تأیید بررسی کنید"
|
||||
verification-code-text: کد تأییدی که از طریق ایمیل دریافت کردهاید را وارد کنید
|
||||
verification-input-label: کد تأیید
|
||||
verify-button-text: تأیید و ادامه
|
||||
didnt-receive-a-code: هنوز کدی دریافت نکردهاید؟
|
||||
not-sent:
|
||||
message: هنوز کدی دریافت نکردهاید؟
|
||||
alternative-choice: از گزینههای جایگزین زیر برای تأیید حساب خود استفاده کنید
|
||||
resend-email: ارسال مجدد ایمیل
|
||||
send-by-sms: ارسال از طریق پیامک
|
||||
two-fa:
|
||||
verify-code:
|
||||
verification-code: کد تأیید
|
||||
check-title: ایمیل یا تلفن خود را بررسی کنید
|
||||
check-subtitle: ما یک کد تأیید برای شما ارسال کردهایم. کد را در زیر وارد کنید تا فرآیند ورود کامل شود
|
||||
expired-help-text: کد احراز هویت دو مرحلهای شما منقضی شده است. در صورت نیاز میتوانید کد دیگری درخواست کنید
|
||||
resend-code: ارسال مجدد کد
|
||||
verify-code: تأیید کد
|
||||
cancel-two-factor: لغو احراز هویت دو مرحلهای
|
||||
invalid-session-error-notification: جلسه نامعتبر است. لطفاً دوباره امتحان کنید
|
||||
verification-successful-notification: تأیید با موفقیت انجام شد!
|
||||
verification-code-expired-notification: کد تأیید منقضی شده است. لطفاً کد جدیدی درخواست کنید
|
||||
verification-code-failed-notification: تأیید ناموفق بود. لطفاً دوباره امتحان کنید
|
||||
resend-code:
|
||||
verification-code-resent-notification: کد تأیید جدید ارسال شد
|
||||
verification-code-resent-error-notification: خطا در ارسال مجدد کد تأیید. لطفاً دوباره امتحان کنید
|
||||
forgot-password:
|
||||
success-message: ایمیل خود را برای ادامه بررسی کنید!
|
||||
is-sent:
|
||||
title: تقریباً تمام شد!
|
||||
message: "<strong>ایمیل خود را بررسی کنید!</strong><br> ما یک لینک جادویی به ایمیل شما ارسال کردهایم که به شما امکان بازنشانی رمز عبور را میدهد. این لینک در 15 دقیقه منقضی میشود"
|
||||
not-sent:
|
||||
title: رمز عبور خود را فراموش کردهاید؟
|
||||
message: "<strong>نگران نباشید، ما از شما پشتیبانی میکنیم.</strong><br> ایمیلی که برای ورود به {appName} استفاده میکنید را وارد کنید و ما یک لینک امن برای بازنشانی رمز عبور برای شما ارسال خواهیم کرد"
|
||||
form:
|
||||
email-label: آدرس ایمیل شما
|
||||
submit-button: خوب، لینک جادویی را برایم بفرست!
|
||||
nevermind-button: بیخیال
|
||||
login:
|
||||
title: به حساب کاربری خود وارد شوید
|
||||
no-identity-notification: آیا فراموش کردید ایمیل خود را وارد کنید؟
|
||||
no-password-notification: آیا فراموش کردید رمز عبور خود را وارد کنید؟
|
||||
unverified-notification: حساب شما برای ادامه باید تأیید شود
|
||||
password-reset-required: برای ادامه، بازنشانی رمز عبور لازم است
|
||||
failed-attempt:
|
||||
message: "<strong>رمز عبور خود را فراموش کردهاید؟</strong><br> برای بازنشانی رمز عبور خود روی دکمه زیر کلیک کنید"
|
||||
button-text: خوب، به من کمک کن تا بازنشانی کنم!
|
||||
form:
|
||||
email-label: آدرس ایمیل
|
||||
password-label: رمز عبور
|
||||
remember-me-label: مرا به خاطر بسپار
|
||||
forgot-password-label: رمز عبور خود را فراموش کردهاید؟
|
||||
sign-in-button: ورود
|
||||
create-account-button: ایجاد حساب جدید
|
||||
slow-connection-message: در حال تجربه مشکلات اتصال
|
||||
reset-password:
|
||||
success-message: رمز عبور شما بازنشانی شد! برای ادامه وارد شوید
|
||||
invalid-verification-code: این لینک بازنشانی رمز عبور نامعتبر یا منقضی شده است
|
||||
title: بازنشانی رمز عبور
|
||||
form:
|
||||
code:
|
||||
label: کد بازنشانی شما
|
||||
help-text: کد تأییدی که در ایمیل خود دریافت کردهاید
|
||||
password:
|
||||
label: رمز عبور جدید
|
||||
help-text: برای ادامه، رمزی با حداقل 6 کاراکتر وارد کنید
|
||||
confirm-password:
|
||||
label: تأیید رمز عبور جدید
|
||||
help-text: برای ادامه، رمزی با حداقل 6 کاراکتر وارد کنید
|
||||
submit-button: بازنشانی رمز عبور
|
||||
back-button: بازگشت
|
||||
console:
|
||||
create-or-join-organization:
|
||||
modal-title: ایجاد یا پیوستن به یک سازمان
|
||||
join-success-notification: شما به یک سازمان جدید پیوستید!
|
||||
create-success-notification: شما یک سازمان جدید ایجاد کردید!
|
||||
switch-organization:
|
||||
modal-title: آیا مطمئن هستید که میخواهید سازمان را به {organizationName} تغییر دهید؟
|
||||
modal-body: با تأیید، حساب شما همچنان وارد خواهد ماند، اما سازمان اصلی شما تغییر خواهد کرد
|
||||
modal-accept-button-text: بله، میخواهم سازمان را تغییر دهم
|
||||
success-notification: شما سازمانها را تغییر دادید
|
||||
account:
|
||||
index:
|
||||
upload-new: بارگذاری جدید
|
||||
phone: شماره تلفن شما
|
||||
photos: عکسها
|
||||
timezone: منطقه زمانی خود را انتخاب کنید
|
||||
admin:
|
||||
menu:
|
||||
overview: مرور
|
||||
organizations: سازمانها
|
||||
branding: برندینگ
|
||||
2fa-config: پیکربندی 2FA
|
||||
schedule-monitor: مانیتور برنامه
|
||||
services: خدمات
|
||||
mail: ایمیل
|
||||
filesystem: سیستم فایل
|
||||
queue: صف
|
||||
socket: سوکت
|
||||
push-notifications: اعلانهای فشاری
|
||||
schedule-monitor:
|
||||
schedule-monitor: مانیتور برنامه
|
||||
task-logs-for: "لاگهای وظایف برای:"
|
||||
showing-last-count: نمایش آخرین {count} لاگ
|
||||
name: نام
|
||||
type: نوع
|
||||
timezone: منطقه زمانی
|
||||
last-started: آخرین شروع
|
||||
last-finished: آخرین پایان
|
||||
last-failure: آخرین شکست
|
||||
date: تاریخ
|
||||
memory: حافظه
|
||||
runtime: زمان اجرا
|
||||
output: خروجی
|
||||
no-output: بدون خروجی
|
||||
config:
|
||||
database:
|
||||
title: پیکربندی پایگاه داده
|
||||
filesystem:
|
||||
title: پیکربندی سیستم فایل
|
||||
mail:
|
||||
title: پیکربندی ایمیل
|
||||
notification-channels:
|
||||
title: پیکربندی اعلانهای فشاری
|
||||
queue:
|
||||
title: پیکربندی صف
|
||||
services:
|
||||
title: پیکربندی خدمات
|
||||
socket:
|
||||
title: پیکربندی سوکت
|
||||
branding:
|
||||
title: برندینگ
|
||||
icon-text: آیکون
|
||||
upload-new: بارگذاری جدید
|
||||
reset-default: بازنشانی به پیشفرض
|
||||
logo-text: لوگو
|
||||
theme: تم پیشفرض
|
||||
index:
|
||||
total-users: مجموع کاربران
|
||||
total-organizations: مجموع سازمانها
|
||||
total-transactions: مجموع تراکنشها
|
||||
notifications:
|
||||
title: اعلانها
|
||||
notification-settings: تنظیمات اعلان
|
||||
organizations:
|
||||
index:
|
||||
title: سازمانها
|
||||
owner-name-column: مالک
|
||||
owner-phone-column: تلفن مالک
|
||||
owner-email-column: ایمیل مالک
|
||||
users-count-column: کاربران
|
||||
phone-column: تلفن
|
||||
email-column: ایمیل
|
||||
users:
|
||||
title: کاربران
|
||||
settings:
|
||||
index:
|
||||
title: تنظیمات سازمان
|
||||
organization-name: نام سازمان
|
||||
organization-description: توضیحات سازمان
|
||||
organization-phone: شماره تلفن سازمان
|
||||
organization-currency: ارز سازمان
|
||||
organization-id: شناسه سازمان
|
||||
organization-branding: برندینگ سازمان
|
||||
logo: لوگو
|
||||
logo-help-text: لوگو برای سازمان شما
|
||||
upload-new-logo: بارگذاری لوگوی جدید
|
||||
backdrop: پسزمینه
|
||||
backdrop-help-text: بنر یا تصویر پسزمینه اختیاری برای سازمان شما
|
||||
upload-new-backdrop: بارگذاری پسزمینه جدید
|
||||
organization-timezone: منطقه زمانی پیشفرض برای سازمان خود را انتخاب کنید
|
||||
select-timezone: انتخاب منطقه زمانی
|
||||
extensions:
|
||||
title: افزونهها به زودی میآیند!
|
||||
message: لطفاً در نسخههای آینده بررسی کنید، زیرا ما در حال آمادهسازی برای راهاندازی مخزن و بازار افزونهها هستیم
|
||||
notifications:
|
||||
select-all: انتخاب همه
|
||||
mark-as-read: علامتگذاری به عنوان خواندهشده
|
||||
received: "دریافتشده:"
|
||||
message: هیچ اعلانی برای نمایش وجود ندارد
|
||||
invite:
|
||||
for-users:
|
||||
invitation-message: شما به پیوستن به {companyName} دعوت شدهاید
|
||||
invitation-sent-message: شما به پیوستن به سازمان {companyName} در {appName} دعوت شدهاید. برای پذیرش این دعوت، کد دعوت دریافتشده از طریق ایمیل را وارد کنید و روی ادامه کلیک کنید
|
||||
invitation-code-sent-text: کد دعوت شما
|
||||
accept-invitation-text: پذیرش دعوت
|
||||
onboard:
|
||||
index:
|
||||
title: ایجاد حساب کاربری شما
|
||||
welcome-title: "<strong>به {companyName} خوش آمدید!</strong><br />"
|
||||
welcome-text: جزئیات مورد نیاز زیر را تکمیل کنید تا شروع کنید
|
||||
full-name: نام کامل
|
||||
full-name-help-text: نام کامل شما
|
||||
your-email: آدرس ایمیل
|
||||
your-email-help-text: آدرس ایمیل شما
|
||||
phone: شماره تلفن
|
||||
phone-help-text: شماره تلفن شما
|
||||
organization-name: نام سازمان
|
||||
organization-help-text: نام سازمان شما، تمام خدمات و منابع شما تحت این سازمان مدیریت خواهند شد، بعداً میتوانید به تعداد دلخواه سازمان ایجاد کنید
|
||||
password: یک رمز عبور وارد کنید
|
||||
password-help-text: رمز عبور شما، مطمئن شوید که قوی است
|
||||
confirm-password: رمز عبور خود را تأیید کنید
|
||||
confirm-password-help-text: فقط برای تأیید رمز عبوری که در بالا وارد کردهاید
|
||||
continue-button-text: ادامه
|
||||
verify-email:
|
||||
header-title: تأیید حساب
|
||||
title: آدرس ایمیل خود را تأیید کنید
|
||||
message-text: "<strong>تقریباً تمام شد!</strong><br> ایمیل خود را برای دریافت کد تأیید بررسی کنید"
|
||||
verification-code-text: کد تأییدی که از طریق ایمیل دریافت کردهاید را وارد کنید
|
||||
verification-input-label: کد تأیید
|
||||
verify-button-text: تأیید و ادامه
|
||||
didnt-receive-a-code: هنوز کدی دریافت نکردهاید؟
|
||||
not-sent:
|
||||
message: هنوز کدی دریافت نکردهاید؟
|
||||
alternative-choice: از گزینههای جایگزین زیر برای تأیید حساب خود استفاده کنید
|
||||
resend-email: ارسال مجدد ایمیل
|
||||
send-by-sms: ارسال از طریق پیامک
|
||||
install:
|
||||
installer-header: نصبکننده
|
||||
failed-message-sent: نصب ناموفق بود! برای تلاش مجدد روی دکمه زیر کلیک کنید
|
||||
retry-install: تلاش مجدد برای نصب
|
||||
start-install: شروع نصب
|
||||
layout:
|
||||
header:
|
||||
menus:
|
||||
organization:
|
||||
settings: تنظیمات سازمان
|
||||
create-or-join: ایجاد یا پیوستن به سازمانها
|
||||
explore-extensions: کاوش در افزونهها
|
||||
user:
|
||||
view-profile: مشاهده پروفایل
|
||||
keyboard-shortcuts: نمایش میانبرهای صفحهکلید
|
||||
changelog: تغییرات
|
||||
25
docker-compose.override.yml.example
Normal file
25
docker-compose.override.yml.example
Normal file
@@ -0,0 +1,25 @@
|
||||
# Docker Compose Override Example
|
||||
# Copy this file to docker-compose.override.yml and customize for your environment
|
||||
|
||||
version: "3.8"
|
||||
services:
|
||||
application:
|
||||
environment:
|
||||
CONSOLE_HOST: http://localhost:4200
|
||||
# Add your environment-specific variables here
|
||||
MAIL_MAILER: smtp # or ses, mailgun, postmark, sendgrid
|
||||
OSRM_HOST: https://router.project-osrm.org
|
||||
# IPINFO_API_KEY: your_api_key
|
||||
# GOOGLE_MAPS_API_KEY: your_api_key
|
||||
# GOOGLE_MAPS_LOCALE: us
|
||||
# TWILIO_SID: your_twilio_sid
|
||||
# TWILIO_TOKEN: your_twilio_token
|
||||
# TWILIO_FROM: your_twilio_phone
|
||||
|
||||
socket:
|
||||
environment:
|
||||
# DEVELOPMENT: Allow localhost connections (HTTP, HTTPS, and WebSocket protocols)
|
||||
SOCKETCLUSTER_OPTIONS: '{"origins":"http://localhost:*,https://localhost:*,ws://localhost:*,wss://localhost:*"}'
|
||||
|
||||
# PRODUCTION: Replace with your actual domain(s) - include all protocols
|
||||
# SOCKETCLUSTER_OPTIONS: '{"origins":"https://yourdomain.com:*,wss://yourdomain.com:*,https://app.yourdomain.com:*,wss://app.yourdomain.com:*"}'
|
||||
@@ -27,12 +27,16 @@ services:
|
||||
|
||||
socket:
|
||||
image: socketcluster/socketcluster:v17.4.0
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "38000:8000"
|
||||
environment:
|
||||
SOCKETCLUSTER_WORKERS: 10
|
||||
SOCKETCLUSTER_BROKERS: 10
|
||||
# SOCKETCLUSTER_OPTIONS can be set via docker-compose.override.yml for specific environments
|
||||
# For production, use: SOCKETCLUSTER_OPTIONS: '{"origins":"https://yourdomain.com:*"}'
|
||||
# For development, use: SOCKETCLUSTER_OPTIONS: '{"origins":"http://localhost:*"}'
|
||||
|
||||
scheduler:
|
||||
image: fleetbase/fleetbase-api:latest
|
||||
|
||||
@@ -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.18
|
||||
ENV FLEETBASE_VERSION=0.7.23
|
||||
|
||||
# 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"]
|
||||
|
||||
Submodule packages/core-api updated: 075d33388c...b637c41e98
Submodule packages/dev-engine updated: e1ee297bf6...f11d032cb8
Submodule packages/ember-core updated: fae4a4f41d...ac75087bb0
Submodule packages/ember-ui updated: cb43182a11...f440950231
Submodule packages/fleetops updated: 91f84efedc...66edd91bab
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user