mirror of
https://github.com/fleetbase/fleetbase.git
synced 2026-01-06 22:48:19 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50f30742a8 | ||
|
|
c7b1a876f5 | ||
|
|
892eaeeca0 | ||
|
|
32f4b69697 | ||
|
|
6317c4b2e4 | ||
|
|
e7c229ece5 | ||
|
|
983a3d22b5 | ||
|
|
42105380ca | ||
|
|
8fd4a40016 | ||
|
|
331e98af20 | ||
|
|
b1d226256a | ||
|
|
f1ee8b0c99 | ||
|
|
23d5ecfdb8 | ||
|
|
2795a2f1be | ||
|
|
69afdee975 | ||
|
|
60845b9953 | ||
|
|
f3997a1bb7 | ||
|
|
23a691e7e7 | ||
|
|
3dc562987a |
@@ -9,10 +9,10 @@
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"require": {
|
||||
"php": "^8.0",
|
||||
"fleetbase/core-api": "^1.5.9",
|
||||
"fleetbase/fleetops-api": "^0.5.8",
|
||||
"fleetbase/registry-bridge": "^0.0.14",
|
||||
"fleetbase/storefront-api": "^0.3.15",
|
||||
"fleetbase/core-api": "^1.5.15",
|
||||
"fleetbase/fleetops-api": "^0.5.12",
|
||||
"fleetbase/registry-bridge": "^0.0.17",
|
||||
"fleetbase/storefront-api": "^0.3.16",
|
||||
"guzzlehttp/guzzle": "^7.0.1",
|
||||
"laravel/framework": "^10.0",
|
||||
"laravel/octane": "^2.3",
|
||||
|
||||
412
api/composer.lock
generated
412
api/composer.lock
generated
File diff suppressed because it is too large
Load Diff
12
console/app/components/modals/edit-organization.hbs
Normal file
12
console/app/components/modals/edit-organization.hbs
Normal file
@@ -0,0 +1,12 @@
|
||||
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
|
||||
<div class="modal-body-container pt-0i text-gray-900 dark:text-white">
|
||||
<InputGroup @name="Organization name" @value={{@options.organization.name}} />
|
||||
<InputGroup @name="Organization description" @value={{@options.organization.description}} />
|
||||
<InputGroup @name="Organization phone number">
|
||||
<PhoneInput @value={{@options.organization.phone}} @onInput={{fn (mut @options.organization.phone)}} class="form-input w-full" />
|
||||
</InputGroup>
|
||||
<InputGroup @name="Organization currency">
|
||||
<CurrencySelect @value={{@options.organization.currency}} @onSelect={{fn (mut @options.organization.currency)}} @triggerClass="w-full form-select" />
|
||||
</InputGroup>
|
||||
</div>
|
||||
</Modal::Default>
|
||||
3
console/app/components/modals/edit-organization.js
Normal file
3
console/app/components/modals/edit-organization.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Component from '@glimmer/component';
|
||||
|
||||
export default class ModalsEditOrganizationComponent extends Component {}
|
||||
43
console/app/components/modals/leave-organization.hbs
Normal file
43
console/app/components/modals/leave-organization.hbs
Normal file
@@ -0,0 +1,43 @@
|
||||
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
|
||||
<div class="modal-body-container pt-0i text-gray-900 dark:text-white">
|
||||
{{#if @options.isOwner}}
|
||||
{{#if @options.hasOtherMembers}}
|
||||
<p>
|
||||
<div class="text-base mb-2">
|
||||
As the owner of
|
||||
<strong>{{@options.organization.name}}</strong>, leaving the organization requires you to nominate a new owner.
|
||||
</div>
|
||||
<div>Please select a member from the dropdown below to transfer ownership before you can proceed.</div>
|
||||
</p>
|
||||
<InputGroup @name="Select a New Owner" @wrapperClass="mt-2 mb-0i">
|
||||
<Select
|
||||
@options={{@options.organization.users}}
|
||||
@value={{@options.newOwnerId}}
|
||||
@onSelect={{@options.selectNewOwner}}
|
||||
@optionLabel="name"
|
||||
@optionValue="id"
|
||||
@placeholder="Select a member"
|
||||
/>
|
||||
</InputGroup>
|
||||
{{else if @options.willBeDeleted}}
|
||||
<p>
|
||||
<div class="text-base mb-2">
|
||||
You are the sole owner of
|
||||
<strong>{{@options.organization.name}}</strong>.
|
||||
</div>
|
||||
<div>By leaving, the organization will be permanently deleted along with all its data.</div>
|
||||
<div>Are you sure you want to proceed?</div>
|
||||
</p>
|
||||
<p class="mt-3"><em>This action cannot be undone.</em></p>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<p>
|
||||
<div class="text-base mb-2">
|
||||
Are you sure you want to leave the organization
|
||||
<strong>{{@options.organization.name}}</strong>?
|
||||
</div>
|
||||
<div>You will no longer have access to its resources and settings.</div>
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</Modal::Default>
|
||||
3
console/app/components/modals/leave-organization.js
Normal file
3
console/app/components/modals/leave-organization.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Component from '@glimmer/component';
|
||||
|
||||
export default class ModalsLeaveOrganizationComponent extends Component {}
|
||||
@@ -161,7 +161,7 @@ export default class ConsoleController extends Controller {
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to invalidate and log user out
|
||||
* Action to create or join an organization.
|
||||
*
|
||||
* @void
|
||||
*/
|
||||
|
||||
203
console/app/controllers/console/account/organizations.js
Normal file
203
console/app/controllers/console/account/organizations.js
Normal file
@@ -0,0 +1,203 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { later } from '@ember/runloop';
|
||||
import { htmlSafe } from '@ember/template';
|
||||
|
||||
export default class ConsoleAccountOrganizationsController extends Controller {
|
||||
@service currentUser;
|
||||
@service modalsManager;
|
||||
@service crud;
|
||||
@service notifications;
|
||||
@service intl;
|
||||
@service fetch;
|
||||
@service router;
|
||||
|
||||
@action async leaveOrganization(organization) {
|
||||
const isOwner = this.currentUser.id === organization.owner_uuid;
|
||||
const hasOtherMembers = organization.users_count > 1;
|
||||
const willBeDeleted = isOwner && organization.users_count === 1;
|
||||
|
||||
if (this.model.length === 1) {
|
||||
return this.notifications.warning('Unable to leave your only organization.');
|
||||
}
|
||||
|
||||
if (hasOtherMembers) {
|
||||
organization.loadUsers({ exclude: [this.currentUser.id] });
|
||||
}
|
||||
|
||||
this.modalsManager.show('modals/leave-organization', {
|
||||
title: isOwner ? (willBeDeleted ? 'Delete Organization' : 'Transfer Ownership and Leave') : 'Leave Organization',
|
||||
acceptButtonText: isOwner ? (willBeDeleted ? 'Delete Organization' : 'Transfer Ownership and Leave') : 'Leave Organization',
|
||||
acceptButtonScheme: 'danger',
|
||||
acceptButtonIcon: isOwner ? (willBeDeleted ? 'trash' : 'person-walking-arrow-right') : 'person-walking-arrow-right',
|
||||
acceptButtonDisabled: isOwner && hasOtherMembers,
|
||||
isOwner,
|
||||
hasOtherMembers,
|
||||
willBeDeleted,
|
||||
organization,
|
||||
newOwnerId: null,
|
||||
selectNewOwner: (newOwnerId) => {
|
||||
this.modalsManager.setOption('newOwnerId', newOwnerId);
|
||||
this.modalsManager.setOption('acceptButtonDisabled', false);
|
||||
},
|
||||
confirm: async (modal) => {
|
||||
modal.startLoading();
|
||||
|
||||
if (isOwner) {
|
||||
if (hasOtherMembers) {
|
||||
const newOwnerId = this.modalsManager.getOption('newOwnerId');
|
||||
try {
|
||||
await organization.transferOwnership(newOwnerId, { leave: true });
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
|
||||
return this.router.refresh();
|
||||
}
|
||||
|
||||
if (willBeDeleted) {
|
||||
try {
|
||||
await organization.destroyRecord();
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
|
||||
return this.router.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await organization.leave();
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
|
||||
return this.router.refresh();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action switchOrganization(organization) {
|
||||
this.modalsManager.confirm({
|
||||
title: this.intl.t('console.switch-organization.modal-title', { organizationName: organization.name }),
|
||||
body: this.intl.t('console.switch-organization.modal-body'),
|
||||
acceptButtonText: this.intl.t('console.switch-organization.modal-accept-button-text'),
|
||||
acceptButtonScheme: 'primary',
|
||||
confirm: async (modal) => {
|
||||
modal.startLoading();
|
||||
|
||||
try {
|
||||
await this.fetch.post('auth/switch-organization', { next: organization.uuid });
|
||||
this.fetch.flushRequestCache('auth/organizations');
|
||||
this.notifications.success(this.intl.t('console.switch-organization.success-notification'));
|
||||
return later(
|
||||
this,
|
||||
() => {
|
||||
window.location.reload();
|
||||
},
|
||||
900
|
||||
);
|
||||
} catch (error) {
|
||||
modal.stopLoading();
|
||||
return this.notifications.serverError(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action deleteOrganization(organization) {
|
||||
const isOwner = this.currentUser.id === organization.owner_uuid;
|
||||
|
||||
if (this.model.length === 1) {
|
||||
return this.notifications.warning('Unable to delete your only organization.');
|
||||
}
|
||||
|
||||
if (!isOwner) {
|
||||
return this.notifications.warning('You do not have rights to delete this organization.');
|
||||
}
|
||||
|
||||
this.crud.delete(organization, {
|
||||
title: `Are you sure you want to delete the organization ${organization.name}?`,
|
||||
body: htmlSafe(
|
||||
`This action will permanently remove all data, including orders, members, and settings associated with the organization. <br /><br /><strong>This action cannot be undone.</strong>`
|
||||
),
|
||||
acceptButtonText: 'Delete Organization',
|
||||
acceptButtonScheme: 'danger',
|
||||
acceptButtonIcon: 'trash',
|
||||
confirm: async (modal) => {
|
||||
modal.startLoading();
|
||||
|
||||
try {
|
||||
await organization.destroyRecord();
|
||||
return this.router.refresh();
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action editOrganization(organization) {
|
||||
this.modalsManager.show('modals/edit-organization', {
|
||||
title: 'Edit Organization',
|
||||
acceptButtonText: 'Save Changes',
|
||||
acceptButtonIcon: 'save',
|
||||
isOwner: this.currentUser.id === organization.owner_uuid,
|
||||
organization,
|
||||
confirm: async (modal) => {
|
||||
modal.startLoading();
|
||||
|
||||
try {
|
||||
await organization.save();
|
||||
return this.router.refresh();
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action createOrganization() {
|
||||
const currency = this.currentUser.currency;
|
||||
const country = this.currentUser.country;
|
||||
|
||||
this.modalsManager.show('modals/edit-organization', {
|
||||
title: 'Create Organization',
|
||||
acceptButtonText: this.intl.t('common.confirm'),
|
||||
acceptButtonIcon: 'check',
|
||||
acceptButtonIconPrefix: 'fas',
|
||||
organization: {
|
||||
name: null,
|
||||
decription: null,
|
||||
phone: null,
|
||||
currency,
|
||||
country,
|
||||
timezone: null,
|
||||
},
|
||||
confirm: async (modal) => {
|
||||
modal.startLoading();
|
||||
|
||||
const organization = modal.getOption('organization');
|
||||
const { name, description, phone, currency, country, timezone } = organization;
|
||||
|
||||
try {
|
||||
await this.fetch.post('auth/create-organization', {
|
||||
name,
|
||||
description,
|
||||
phone,
|
||||
currency,
|
||||
country,
|
||||
timezone,
|
||||
});
|
||||
this.fetch.flushRequestCache('auth/organizations');
|
||||
this.notifications.success(this.intl.t('console.create-or-join-organization.create-success-notification'));
|
||||
return this.router.refresh();
|
||||
} catch (error) {
|
||||
modal.stopLoading();
|
||||
return this.notifications.serverError(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
export function initialize(owner) {
|
||||
const universe = owner.lookup('service:universe');
|
||||
export function initialize(application) {
|
||||
const universe = application.lookup('service:universe');
|
||||
if (universe) {
|
||||
universe.createRegistries(['@fleetbase/console', 'auth:login']);
|
||||
universe.bootEngines(owner);
|
||||
universe.bootEngines(application);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export function initialize(owner) {
|
||||
const leafletService = owner.lookup('service:leaflet');
|
||||
export function initialize(application) {
|
||||
const leafletService = application.lookup('service:leaflet');
|
||||
if (leafletService) {
|
||||
leafletService.load({
|
||||
onReady: function (L) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Model, { attr, belongsTo } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
import autoSerialize from '../utils/auto-serialize';
|
||||
|
||||
@@ -34,6 +35,7 @@ export default class Company extends Model {
|
||||
@attr('string') slug;
|
||||
|
||||
/** @dates */
|
||||
@attr('date') joined_at;
|
||||
@attr('date') deleted_at;
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
@@ -71,4 +73,32 @@ export default class Company extends Model {
|
||||
toJSON() {
|
||||
return autoSerialize(this);
|
||||
}
|
||||
|
||||
async transferOwnership(newOwner, params = {}) {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
return fetch.post('companies/transfer-ownership', { company: this.id, newOwner, ...params });
|
||||
}
|
||||
|
||||
async leave(user = null, params = {}) {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
return fetch.post('companies/leave', { company: this.id, user, ...params });
|
||||
}
|
||||
|
||||
async loadUsers(params = {}) {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const users = await fetch.get(`companies/${this.id}/users`, { ...params }, { normalizeToEmberData: true, normalizeModelType: 'user' });
|
||||
this.set('users', users);
|
||||
return users;
|
||||
} catch (error) {
|
||||
this.set('users', []);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,17 @@ export default class UserModel extends Model {
|
||||
});
|
||||
}
|
||||
|
||||
verify() {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
return fetch.patch(`users/verify/${this.id}`).then((response) => {
|
||||
this.email_verified_at = response.email_verified_at;
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
removeFromCurrentCompany() {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
@@ -7,15 +7,18 @@ export default class Router extends EmberRouter {
|
||||
}
|
||||
|
||||
Router.map(function () {
|
||||
this.route('virtual', { path: '/:slug' });
|
||||
this.route('install');
|
||||
this.route('onboard', function () {
|
||||
this.route('verify-email');
|
||||
});
|
||||
this.route('auth', function () {
|
||||
this.route('login', { path: '/' });
|
||||
this.route('forgot-password');
|
||||
this.route('reset-password', { path: '/reset-password/:id' });
|
||||
this.route('two-fa');
|
||||
this.route('verification');
|
||||
});
|
||||
this.route('onboard', function () {
|
||||
this.route('verify-email');
|
||||
this.route('portal-login', { path: '/portal' });
|
||||
});
|
||||
this.route('invite', { path: 'join' }, function () {
|
||||
this.route('for-driver', { path: '/fleet/:public_id' });
|
||||
@@ -23,24 +26,23 @@ Router.map(function () {
|
||||
});
|
||||
this.route('console', { path: '/' }, function () {
|
||||
this.route('home', { path: '/' });
|
||||
this.route('extensions');
|
||||
this.route('notifications');
|
||||
this.route('account', function () {
|
||||
this.route('virtual', { path: '/:slug/:view' });
|
||||
this.route('virtual', { path: '/:slug' });
|
||||
this.route('auth');
|
||||
});
|
||||
this.route('settings', function () {
|
||||
this.route('virtual', { path: '/:slug/:view' });
|
||||
this.route('virtual', { path: '/:slug' });
|
||||
this.route('two-fa');
|
||||
});
|
||||
this.route('virtual', { path: '/:slug/:view' });
|
||||
this.route('virtual', { path: '/:slug' });
|
||||
this.route('admin', function () {
|
||||
this.route('config', function () {
|
||||
this.route('database');
|
||||
this.route('cache');
|
||||
this.route('filesystem');
|
||||
this.route('mail');
|
||||
this.route('notification-channels');
|
||||
this.route('notification-channels', { path: '/push-notifications' });
|
||||
this.route('queue');
|
||||
this.route('services');
|
||||
this.route('socket');
|
||||
@@ -48,12 +50,16 @@ Router.map(function () {
|
||||
this.route('branding');
|
||||
this.route('notifications');
|
||||
this.route('two-fa-settings');
|
||||
this.route('virtual', { path: '/:slug/:view' });
|
||||
this.route('virtual', { path: '/:slug' });
|
||||
this.route('organizations', function () {
|
||||
this.route('index', { path: '/' });
|
||||
this.route('users', { path: '/:company_id' });
|
||||
this.route('index', { path: '/' }, function () {
|
||||
this.route('users', { path: '/:public_id/users' });
|
||||
});
|
||||
});
|
||||
this.route('schedule-monitor', function () {
|
||||
this.route('logs', { path: '/:id/logs' });
|
||||
});
|
||||
});
|
||||
});
|
||||
this.route('install');
|
||||
this.route('catch', { path: '/*' });
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import isElectron from '@fleetbase/ember-core/utils/is-electron';
|
||||
import pathToRoute from '@fleetbase/ember-core/utils/path-to-route';
|
||||
|
||||
@@ -16,6 +17,36 @@ export default class ApplicationRoute extends Route {
|
||||
@service universe;
|
||||
@tracked defaultTheme;
|
||||
|
||||
/**
|
||||
* Handle the transition into the application.
|
||||
*
|
||||
* @memberof ApplicationRoute
|
||||
*/
|
||||
@action willTransition(transition) {
|
||||
this.universe.callHooks('application:will-transition', this.session, this.router, transition);
|
||||
}
|
||||
|
||||
/**
|
||||
* On application route activation
|
||||
*
|
||||
* @memberof ApplicationRoute
|
||||
* @void
|
||||
*/
|
||||
@action activate() {
|
||||
this.initializeTheme();
|
||||
this.initializeLocale();
|
||||
}
|
||||
|
||||
/**
|
||||
* The application loading event.
|
||||
* Here will just run extension hooks.
|
||||
*
|
||||
* @memberof ApplicationRoute
|
||||
*/
|
||||
@action loading(transition) {
|
||||
this.universe.callHooks('application:loading', this.session, this.router, transition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the installation status of Fleetbase and transition user accordingly.
|
||||
*
|
||||
@@ -45,27 +76,18 @@ export default class ApplicationRoute extends Route {
|
||||
* @return {Transition}
|
||||
* @memberof ApplicationRoute
|
||||
*/
|
||||
async beforeModel() {
|
||||
async beforeModel(transition) {
|
||||
await this.session.setup();
|
||||
await this.universe.booting();
|
||||
|
||||
this.universe.callHooks('application:before-model', this.session, this.router, transition);
|
||||
|
||||
const shift = this.urlSearchParams.get('shift');
|
||||
if (this.session.isAuthenticated && shift) {
|
||||
return this.router.transitionTo(pathToRoute(shift));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On application route activation
|
||||
*
|
||||
* @memberof ApplicationRoute
|
||||
* @void
|
||||
*/
|
||||
activate() {
|
||||
this.initializeTheme();
|
||||
this.initializeLocale();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the application's theme settings, applying necessary class names and default theme configurations.
|
||||
*
|
||||
|
||||
10
console/app/routes/catch.js
Normal file
10
console/app/routes/catch.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class CatchRoute extends Route {
|
||||
@service router;
|
||||
|
||||
beforeModel() {
|
||||
return this.router.transitionTo('auth.login');
|
||||
}
|
||||
}
|
||||
10
console/app/routes/console/account/organizations.js
Normal file
10
console/app/routes/console/account/organizations.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ConsoleAccountOrganizationsRoute extends Route {
|
||||
@service currentUser;
|
||||
|
||||
model() {
|
||||
return this.currentUser.loadOrganizations();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<div>
|
||||
<div class="mx-auto w-12 h-12">
|
||||
<LogoIcon @url={{@brand.icon_url}} @size="12" class="mx-auto" />
|
||||
<LogoIcon @size="12" class="mx-auto rounded-sm" />
|
||||
</div>
|
||||
<h2 class="mt-6 mb-3 text-3xl font-extrabold leading-9 text-center text-gray-900 dark:text-gray-100">
|
||||
{{t "auth.login.title"}}
|
||||
|
||||
2
console/app/templates/catch.hbs
Normal file
2
console/app/templates/catch.hbs
Normal file
@@ -0,0 +1,2 @@
|
||||
{{page-title "Catch"}}
|
||||
{{outlet}}
|
||||
@@ -3,6 +3,7 @@
|
||||
<Layout::Sidebar::Panel @open={{true}} @title={{t "common.account"}}>
|
||||
<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|}}
|
||||
<Layout::Sidebar::Item @onClick={{fn this.universe.transitionMenuItem "console.account.virtual" menuItem}} @item={{menuItem}} @icon={{menuItem.icon}}>{{menuItem.title}}</Layout::Sidebar::Item>
|
||||
{{/each}}
|
||||
|
||||
37
console/app/templates/console/account/organizations.hbs
Normal file
37
console/app/templates/console/account/organizations.hbs
Normal file
@@ -0,0 +1,37 @@
|
||||
{{page-title "Organizations"}}
|
||||
<Layout::Section::Header @title="Organizations" />
|
||||
|
||||
<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-4">
|
||||
<div class="flex flex-row justify-end">
|
||||
<Button @type="primary" @icon="plus" @text="Create Organization" @onClick={{this.createOrganization}} />
|
||||
</div>
|
||||
<ContentPanel @title="Your Organizations" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<div class="space-y-2">
|
||||
{{#each @model as |organization|}}
|
||||
<div class="grid grid-cols-3 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 rounded-lg px-3 py-2 items-center">
|
||||
<div>
|
||||
<div class="font-semibold">{{organization.name}}</div>
|
||||
<div>Member Since: {{format-date organization.joined_at}}</div>
|
||||
</div>
|
||||
<div class="col-span-2 flex flex-row items-center justify-end space-x-2">
|
||||
{{#let (eq organization.owner_uuid this.currentUser.id) as |isOwner|}}
|
||||
<Button @type="danger" @size="xs" @icon="person-walking-arrow-right" @text="Leave" @onClick={{fn this.leaveOrganization organization}} />
|
||||
{{#unless (eq this.currentUser.companyId organization.id)}}
|
||||
<Button @size="xs" @icon="shuffle" @text="Switch" @onClick={{fn this.switchOrganization organization}} />
|
||||
{{/unless}}
|
||||
{{#if isOwner}}
|
||||
<Button @size="xs" @icon="pencil" @text="Edit" @onClick={{fn this.editOrganization organization}} />
|
||||
<Button @type="danger" @size="xs" @icon="trash" @text="Delete" @onClick={{fn this.deleteOrganization organization}} />
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</ContentPanel>
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -74,12 +74,6 @@ module.exports = function (environment) {
|
||||
autoClear: true,
|
||||
clearDuration: 1000 * 3.5,
|
||||
},
|
||||
|
||||
'ember-leaflet': {
|
||||
excludeCSS: true,
|
||||
excludeJS: true,
|
||||
excludeImages: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (environment === 'development') {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fleetbase/console",
|
||||
"version": "0.5.8",
|
||||
"version": "0.5.15",
|
||||
"private": true,
|
||||
"description": "Modular logistics and supply chain operating system (LSOS)",
|
||||
"repository": "https://github.com/fleetbase/fleetbase",
|
||||
@@ -28,16 +28,16 @@
|
||||
"test:ember": "ember test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fleetbase/ember-core": "^0.2.19",
|
||||
"@fleetbase/ember-ui": "^0.2.32",
|
||||
"@fleetbase/fleetops-engine": "^0.5.8",
|
||||
"@fleetbase/storefront-engine": "^0.3.15",
|
||||
"@fleetbase/dev-engine": "^0.2.7",
|
||||
"@fleetbase/iam-engine": "^0.1.1",
|
||||
"@fleetbase/registry-bridge-engine": "^0.0.14",
|
||||
"@fleetbase/fleetops-data": "^0.1.18",
|
||||
"@fleetbase/leaflet-routing-machine": "^3.2.16",
|
||||
"@ember/legacy-built-in-components": "^0.4.2",
|
||||
"@fleetbase/dev-engine": "^0.2.8",
|
||||
"@fleetbase/ember-core": "^0.2.21",
|
||||
"@fleetbase/ember-ui": "^0.2.35",
|
||||
"@fleetbase/fleetops-data": "^0.1.18",
|
||||
"@fleetbase/fleetops-engine": "^0.5.12",
|
||||
"@fleetbase/iam-engine": "^0.1.3",
|
||||
"@fleetbase/leaflet-routing-machine": "^3.2.16",
|
||||
"@fleetbase/registry-bridge-engine": "^0.0.17",
|
||||
"@fleetbase/storefront-engine": "^0.3.16",
|
||||
"@fortawesome/ember-fontawesome": "^2.0.0",
|
||||
"ember-changeset": "^4.1.2",
|
||||
"ember-changeset-validations": "^4.1.1",
|
||||
@@ -137,12 +137,9 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@fleetbase/ember-core": "^0.2.19",
|
||||
"@fleetbase/ember-ui": "^0.2.32",
|
||||
"@fleetbase/ember-core": "^0.2.21",
|
||||
"@fleetbase/ember-ui": "^0.2.35",
|
||||
"@fleetbase/fleetops-data": "^0.1.18"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"ember-leaflet@5.1.3": "patches/ember-leaflet@5.1.3.patch"
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
diff --git a/index.js b/index.js
|
||||
index 7361871adfe0b79c70a3699e38cc435f3741cb33..e02f7e6d61605d8b770ed5c0454b6c905d538ff9 100644
|
||||
--- a/index.js
|
||||
+++ b/index.js
|
||||
@@ -1,87 +1,5 @@
|
||||
-'use strict';
|
||||
-const resolve = require('resolve');
|
||||
-const path = require('path');
|
||||
-const mergeTrees = require('broccoli-merge-trees');
|
||||
-const Funnel = require('broccoli-funnel');
|
||||
-const fastbootTransform = require('fastboot-transform');
|
||||
+'use strict'
|
||||
|
||||
module.exports = {
|
||||
- name: require('./package').name,
|
||||
-
|
||||
- treeForVendor() {
|
||||
- let dist = path.join(this.pathBase('leaflet'), 'dist');
|
||||
-
|
||||
- let leafletJs = fastbootTransform(
|
||||
- new Funnel(dist, {
|
||||
- files: ['leaflet-src.js'],
|
||||
- destDir: 'leaflet'
|
||||
- })
|
||||
- );
|
||||
-
|
||||
- let leafletFiles = new Funnel(dist, {
|
||||
- exclude: ['leaflet.js', 'leaflet-src.js', '*.html'],
|
||||
- destDir: 'leaflet'
|
||||
- });
|
||||
-
|
||||
- return mergeTrees([leafletJs, leafletFiles]);
|
||||
- },
|
||||
-
|
||||
- included(app) {
|
||||
- this._super.included.apply(this, arguments);
|
||||
-
|
||||
- // Addon options from the apps ember-cli-build.js
|
||||
- let options = app.options[this.name] || {};
|
||||
-
|
||||
- // If the addon has the _findHost() method (in ember-cli >= 2.7.0), we'll just
|
||||
- // use that.
|
||||
- // if (typeof this._findHost === 'function') {
|
||||
- // app = this._findHost();
|
||||
- // }
|
||||
-
|
||||
- // Otherwise, we'll use this implementation borrowed from the _findHost()
|
||||
- // method in ember-cli.
|
||||
- // Keep iterating upward until we don't have a grandparent.
|
||||
- // Has to do this grandparent check because at some point we hit the project.
|
||||
- let current = this;
|
||||
- do {
|
||||
- if (current.lazyLoading === true || (current.lazyLoading && current.lazyLoading.enabled === true)) {
|
||||
- app = current;
|
||||
- break;
|
||||
- }
|
||||
- app = current.app || app;
|
||||
- } while (current.parent.parent && (current = current.parent));
|
||||
-
|
||||
- if (!options.excludeJS) {
|
||||
- app.import('vendor/leaflet/leaflet-src.js');
|
||||
- }
|
||||
-
|
||||
- // Import leaflet css
|
||||
- if (!options.excludeCSS) {
|
||||
- app.import('vendor/leaflet/leaflet.css');
|
||||
- }
|
||||
-
|
||||
- // Import leaflet images
|
||||
- if (!options.excludeImages) {
|
||||
- let imagesDestDir = '/assets/images';
|
||||
- app.import('vendor/leaflet/images/layers-2x.png', {
|
||||
- destDir: imagesDestDir
|
||||
- });
|
||||
- app.import('vendor/leaflet/images/layers.png', {
|
||||
- destDir: imagesDestDir
|
||||
- });
|
||||
- app.import('vendor/leaflet/images/marker-icon-2x.png', {
|
||||
- destDir: imagesDestDir
|
||||
- });
|
||||
- app.import('vendor/leaflet/images/marker-icon.png', {
|
||||
- destDir: imagesDestDir
|
||||
- });
|
||||
- app.import('vendor/leaflet/images/marker-shadow.png', {
|
||||
- destDir: imagesDestDir
|
||||
- });
|
||||
- }
|
||||
- },
|
||||
-
|
||||
- pathBase(packageName) {
|
||||
- return path.dirname(resolve.sync(packageName + '/package.json', { basedir: __dirname }));
|
||||
- }
|
||||
-};
|
||||
+ name: require('./package').name
|
||||
+}
|
||||
2410
console/pnpm-lock.yaml
generated
2410
console/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ const recast = require('recast');
|
||||
const babelParser = require('recast/parsers/babel');
|
||||
const builders = recast.types.builders;
|
||||
|
||||
function getExtensionMountPath (extensionName) {
|
||||
function getExtensionMountPath(extensionName) {
|
||||
let extensionNameSegments = extensionName.split('/');
|
||||
let mountName = extensionNameSegments[1];
|
||||
|
||||
@@ -16,7 +16,7 @@ function getExtensionMountPath (extensionName) {
|
||||
return mountName.replace('-engine', '');
|
||||
}
|
||||
|
||||
function only (subject, props = []) {
|
||||
function only(subject, props = []) {
|
||||
const keys = Object.keys(subject);
|
||||
const result = {};
|
||||
|
||||
@@ -31,13 +31,13 @@ function only (subject, props = []) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function getExtensions () {
|
||||
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 => {
|
||||
.then((results) => {
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const packagePath = results[i];
|
||||
const packageJson = fs.readFileSync(packagePath);
|
||||
@@ -69,7 +69,7 @@ function getExtensions () {
|
||||
});
|
||||
}
|
||||
|
||||
function getRouterFileContents () {
|
||||
function getRouterFileContents() {
|
||||
const routerFilePath = path.join(__dirname, 'router.map.js');
|
||||
const routerFileContents = fs.readFileSync(routerFilePath, 'utf-8');
|
||||
|
||||
@@ -78,25 +78,25 @@ function getRouterFileContents () {
|
||||
|
||||
(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 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) {
|
||||
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 => {
|
||||
path.value.arguments.forEach((arg) => {
|
||||
if (arg.type === 'FunctionExpression') {
|
||||
functionExpression = arg;
|
||||
}
|
||||
});
|
||||
if (functionExpression) {
|
||||
// Check and add the new engine mounts
|
||||
consoleExtensions.forEach(extension => {
|
||||
consoleExtensions.forEach((extension) => {
|
||||
const mountPath = getExtensionMountPath(extension.name);
|
||||
let route = mountPath;
|
||||
|
||||
@@ -105,7 +105,7 @@ function getRouterFileContents () {
|
||||
}
|
||||
|
||||
// Check if engine is already mounted
|
||||
const isMounted = functionExpression.body.body.some(expressionStatement => {
|
||||
const isMounted = functionExpression.body.body.some((expressionStatement) => {
|
||||
return expressionStatement.expression.arguments[0].value === extension.name;
|
||||
});
|
||||
|
||||
@@ -131,14 +131,14 @@ function getRouterFileContents () {
|
||||
if (path.value.type === 'CallExpression' && path.value.callee.property.name === 'map') {
|
||||
let functionExpression;
|
||||
|
||||
path.value.arguments.forEach(arg => {
|
||||
path.value.arguments.forEach((arg) => {
|
||||
if (arg.type === 'FunctionExpression') {
|
||||
functionExpression = arg;
|
||||
}
|
||||
});
|
||||
|
||||
if (functionExpression) {
|
||||
rootExtensions.forEach(extension => {
|
||||
rootExtensions.forEach((extension) => {
|
||||
const mountPath = getExtensionMountPath(extension.name);
|
||||
let route = mountPath;
|
||||
|
||||
@@ -146,7 +146,7 @@ function getRouterFileContents () {
|
||||
route = extension.fleetbase.route;
|
||||
}
|
||||
|
||||
const isMounted = functionExpression.body.body.some(expressionStatement => {
|
||||
const isMounted = functionExpression.body.body.some((expressionStatement) => {
|
||||
return expressionStatement.expression.arguments[0].value === extension.name;
|
||||
});
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ Router.map(function () {
|
||||
this.route('account', function () {
|
||||
this.route('virtual', { path: '/:slug' });
|
||||
this.route('auth');
|
||||
this.route('organizations');
|
||||
});
|
||||
this.route('settings', function () {
|
||||
this.route('virtual', { path: '/:slug' });
|
||||
@@ -61,4 +62,5 @@ Router.map(function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
this.route('catch', { path: '/*' });
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
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 | modals/edit-organization', 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`<Modals::EditOrganization />`);
|
||||
|
||||
assert.dom().hasText('');
|
||||
|
||||
// Template block usage:
|
||||
await render(hbs`
|
||||
<Modals::EditOrganization>
|
||||
template block text
|
||||
</Modals::EditOrganization>
|
||||
`);
|
||||
|
||||
assert.dom().hasText('template block text');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
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 | modals/leave-organization', 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`<Modals::LeaveOrganization />`);
|
||||
|
||||
assert.dom().hasText('');
|
||||
|
||||
// Template block usage:
|
||||
await render(hbs`
|
||||
<Modals::LeaveOrganization>
|
||||
template block text
|
||||
</Modals::LeaveOrganization>
|
||||
`);
|
||||
|
||||
assert.dom().hasText('template block text');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Controller | console/account/organizations', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:console/account/organizations');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
11
console/tests/unit/routes/catch-test.js
Normal file
11
console/tests/unit/routes/catch-test.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Route | catch', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let route = this.owner.lookup('route:catch');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Route | console/account/organizations', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let route = this.owner.lookup('route:console/account/organizations');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
Submodule packages/core-api updated: 4f7b6797f7...8db60ca179
Submodule packages/dev-engine updated: aa8274dfd5...2ba75a3aec
Submodule packages/ember-core updated: a0781b5e13...4669d8379f
Submodule packages/ember-ui updated: 0c1ff91898...daed82051e
Submodule packages/fleetops updated: 09487c1c50...d2e98638e7
Submodule packages/iam-engine updated: 23dec1cda5...898b8d0d14
Submodule packages/registry-bridge updated: 572c135222...ad14662ff3
Submodule packages/storefront updated: 9f4350d9dd...1a5a035da3
Reference in New Issue
Block a user