@lipagas/storefront-engine
Version:
Headless Commerce & Marketplace Extension for Fleetbase
646 lines (591 loc) • 22.9 kB
JavaScript
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action, set } from '@ember/object';
import { isBlank } from '@ember/utils';
import { timeout } from 'ember-concurrency';
import { task } from 'ember-concurrency-decorators';
import createShareableLink from '../../../../utils/create-shareable-link';
import isEmail from '@lipagas/ember-core/utils/is-email';
import isModel from '@lipagas/ember-core/utils/is-model';
export default class NetworksIndexNetworkStoresController extends Controller {
/**
* Inject the `notifications` service
*
* @var {Service}
* @memberof NetworksIndexNetworkStoresController
*/
notifications;
/**
* Inject the `intl` service
*
* @var {Service}
* @memberof NetworksIndexNetworkStoresController
*/
intl;
/**
* Inject the `modals-manager` service
*
* @var {Service}
* @memberof NetworksIndexNetworkStoresController
*/
modalsManager;
/**
* Inject the `crud` service
*
* @var {Service}
* @memberof NetworksIndexNetworkStoresController
*/
crud;
/**
* Inject the `fetch` service
*
* @var {Service}
* @memberof NetworksIndexNetworkStoresController
*/
fetch;
/**
* Inject the `store` service
*
* @var {Service}
* @memberof NetworksIndexNetworkStoresController
*/
store;
/**
* Inject the `hostRouter` service
*
* @var {Service}
* @memberof NetworksIndexNetworkStoresController
*/
hostRouter;
/**
* Queryable parameters for this controller's model
*
* @var {Array}
* @memberof NetworksIndexNetworkStoresController
*/
queryParams = ['category', 'status', 'storeQuery'];
/**
* The current page of data being viewed
*
* @var {Integer}
* @memberof NetworksIndexNetworkStoresController
*/
page = 1;
/**
* The maximum number of items to show per page
*
* @var {Integer}
*/
limit;
/**
* The search query
*
* @var {String}
* @memberof NetworksIndexNetworkStoresController
*/
storeQuery;
/**
* The param to sort the data on, the param with prepended `-` is descending
*
* @var {String}
* @memberof NetworksIndexNetworkStoresController
*/
sort;
/**
* The param to filter stores by category.
*
* @var {String}
* @memberof NetworksIndexNetworkStoresController
*/
category;
/**
* The current network.
*
* @var {NetworkModel}
* @memberof NetworksIndexNetworkStoresController
*/
network;
/**
* The loading state.
*
* @var {Boolean}
* @memberof NetworksIndexNetworkStoresController
*/
isLoading = false;
/**
* All columns applicable for network stores
*
* @var {Array}
*/
columns = [
{
label: this.intl.t('storefront.networks.index.network.stores.store'),
valuePath: 'name',
width: '130px',
resizable: true,
sortable: true,
filterable: true,
filterComponent: 'filter/string',
showOnlineIndicator: true,
},
{
label: this.intl.t('storefront.common.id'),
valuePath: 'public_id',
cellComponent: 'click-to-copy',
width: '120px',
resizable: true,
sortable: true,
filterable: true,
filterComponent: 'filter/string',
},
{
label: this.intl.t('storefront.common.category'),
valuePath: 'category.name',
cellComponent: 'table/cell/base',
width: '100px',
resizable: true,
sortable: true,
filterable: true,
filterComponent: 'filter/string',
},
{
label: this.intl.t('storefront.common.currency'),
valuePath: 'currency',
cellComponent: 'table/cell/base',
width: '100px',
resizable: true,
sortable: true,
filterable: true,
filterComponent: 'filter/string',
},
{
label: this.intl.t('storefront.networks.index.network.stores.created-at'),
valuePath: 'createdAtShort',
sortParam: 'created_at',
width: '100px',
resizable: true,
sortable: true,
filterable: true,
filterComponent: 'filter/date',
},
{
label: '',
cellComponent: 'table/cell/dropdown',
ddButtonText: false,
ddButtonIcon: 'ellipsis-h',
ddButtonIconPrefix: 'fas',
ddMenuLabel: 'Store Actions',
cellClassNames: 'overflow-visible',
wrapperClass: 'flex items-center justify-end mx-2',
width: '50px',
actions: [
{
label: this.intl.t('storefront.networks.index.network.stores.view-store-details'),
fn: this.viewStoreDetails,
},
{
label: this.intl.t('storefront.networks.index.network.stores.edit-store'),
fn: this.editStore,
},
{
label: this.intl.t('storefront.networks.index.network.stores.assign-category'),
fn: this.assignStoreToCategory,
},
{
separator: true,
},
{
label: this.intl.t('storefront.networks.index.network.stores.remove-store-from-network'),
fn: this.removeStore,
},
],
sortable: false,
filterable: false,
resizable: false,
searchable: false,
},
];
/**
* The search task.
*
* @void
*/
*search({ target: { value } }) {
// if no query don't search
if (isBlank(value)) {
this.storeQuery = null;
return;
}
// timeout for typing
yield timeout(250);
// reset page for results
if (this.page > 1) {
this.page = 1;
}
// update the query param
this.storeQuery = value;
}
/**
* Selects a category and assigns its ID to the current category property.
* If the selected category is null, the category property is set to null.
*
* @action
* @param {CategoryModel|null} selectedCategory - The selected category object containing the ID.
*/
selectCategory(selectedCategory) {
if (selectedCategory) {
this.category = selectedCategory.id;
} else {
this.category = null;
}
}
/**
* Deletes a specified category and moves all stores inside to the top level.
* A confirmation modal is displayed before deletion.
*
* @action
* @param {CategoryModel} category - The category object containing the ID to be deleted.
*/
deleteCategory(category) {
this.modalsManager.confirm({
title: this.intl.t('storefront.networks.index.network.stores.delete-network-category'),
body: this.intl.t('storefront.networks.index.network.stores.deleting-category-move-all-stores-inside-on-top-level'),
confirm: (modal) => {
modal.startLoading();
this.fetch.delete(`networks/${this.network.id}/remove-category`, { category: category.id }, { namespace: 'storefront/int/v1' }).then(() => {
this.categories.removeObject(category);
this.leaveCategory();
modal.done();
});
},
});
}
/**
* Displays a modal to assign a store to a category or create a new category.
* Allows the user to select a category, create a new one, or confirm the assignment.
*
* @action
* @param {StoreModel} store - The store object to be assigned to a category.
* @param {Object} [options={}] - Additional options for the modal.
*/
assignStoreToCategory(store, options = {}) {
this.modalsManager.show('modals/add-store-to-category', {
title: this.intl.t('storefront.networks.index.network.stores.add-store-to-category'),
acceptButtonText: this.intl.t('storefront.networks.index.network.stores.save-change'),
acceptButtonIcon: 'save',
selectedCategory: null,
network: this.network,
onSelectCategory: (category) => {
this.modalsManager.setOption('selectedCategory', category);
},
createNewCategory: (networkCategoriesPicker, parentCategory) => {
this.modalsManager.done();
return this.createNewCategory(networkCategoriesPicker, parentCategory, {
onFinish: () => {
return this.assignStoreToCategory(store);
},
});
},
confirm: (modal) => {
modal.startLoading();
const selectedCategory = this.modalsManager.getOption('selectedCategory');
if (selectedCategory) {
return this.addStoreToCategory(store, selectedCategory).then(() => {
this.notifications.success(`${store.name} category was changed to ${selectedCategory.name}`);
this.hostRouter.refresh();
});
}
modal.done();
},
...options,
});
}
/**
* Sends a POST request to assign a store to a specified category.
* The category and store IDs are sent in the request body.
*
* @action
* @param {StoreModel} store - The store object containing the ID.
* @param {CategoryModel} category - The category object containing the ID.
* @returns {Promise} A promise that resolves when the request is complete.
*/
addStoreToCategory(store, category) {
return this.fetch.post(
`networks/${this.network.id}/set-store-category`,
{
category: category.id,
store: store.id,
},
{ namespace: 'storefront/int/v1' }
);
}
/**
* Creates a new category with specified attributes and displays a modal for editing.
* Allows the user to confirm the creation and save the category.
*
* @action
* @param {NetworkCategoryPickerComponent} networkCategoriesPicker - Picker for network categories.
* @param {ParentCategory} parentCategory - The parent category object, if any.
* @param {Object} [options={}] - Additional options for the modal.
* @returns {Promise} A promise that resolves when the category is created.
*/
createNewCategory(networkCategoriesPicker, parentCategory, options = {}) {
const categoryAttrs = {
owner_uuid: this.network.id,
owner_type: 'storefront:network',
for: 'storefront_network',
};
if (isModel(parentCategory)) {
categoryAttrs.parent_uuid = parentCategory.id;
categoryAttrs.owner_uuid = parentCategory.owner_uuid;
}
const category = this.store.createRecord('category', categoryAttrs);
return this.editCategory(category, {
title: this.intl.t('storefront.networks.index.network.stores.add-new-network-category'),
acceptButtonIcon: 'check',
acceptButtonText: this.intl.t('storefront.networks.index.network.stores.create-new-category'),
successMessage: this.intl.t('storefront.networks.index.network.stores.new-category-created'),
parentCategory,
category,
confirm: (modal) => {
modal.startLoading();
category
.save()
.then((category) => {
this.notifications.success(this.intl.t('storefront.networks.index.network.stores.network-category-create'));
networkCategoriesPicker.categories.pushObject(category);
modal.done();
})
.catch((error) => {
this.notifications.serverError(error);
});
},
...options,
});
}
/**
* Displays a modal to edit a specified category.
* Allows the user to set or clear the parent category, upload an icon, and confirm the changes.
*
* @action
* @param {CategoryModel} category - The category object to be edited.
* @param {Object} [options={}] - Additional options for the modal.
*/
editCategory(category, options = {}) {
this.modalsManager.show('modals/create-network-category', {
title: this.intl.t('storefront.networks.index.network.stores.edit-category', { categoryName: category.name }),
acceptButtonText: this.intl.t('storefront.networks.index.network.stores.save-change'),
acceptButtonIcon: 'save',
iconType: category.icon_file_uuid ? 'image' : 'svg',
network: this.network,
category,
parentCategory: null,
setParentCategory: (parentCategory) => {
this.modalsManager.setOption('parentCategory', parentCategory);
// update on category
category.setProperties({
parent_uuid: parentCategory.id,
});
},
clearImage: () => {
category.setProperties({
icon_file_uuid: null,
icon_url: null,
icon_file: null,
});
},
uploadIcon: (file) => {
this.fetch.uploadFile.perform(
file,
{
path: `uploads/${category.company_uuid}/icons/${category.slug}`,
key_uuid: category.id,
key_type: `category`,
type: `category_icon`,
},
(uploadedFile) => {
category.setProperties({
icon_file_uuid: uploadedFile.id,
icon_url: uploadedFile.url,
icon_file: uploadedFile,
});
}
);
},
confirm: (modal) => {
modal.startLoading();
return category.save().then(() => {
this.notifications.success(options.successMessage ?? 'Category changes saved.');
});
},
...options,
});
}
/**
* Displays a loader and shows a modal to add stores to the network.
* Allows the user to select stores, update the selection, and confirm the addition.
*
* @action
* @returns {Promise} A promise that resolves when the stores are added.
*/
async addStores() {
this.modalsManager.displayLoader();
const { network } = this;
const stores = await this.store.findAll('store');
const members = await network.loadStores();
return this.modalsManager.done().then(() => {
this.modalsManager.show('modals/add-stores-to-network', {
title: this.intl.t('storefront.networks.index.network.stores.add-stores-to-network'),
acceptButtonIcon: 'check',
stores,
members,
selected: members.toArray(),
network,
updateSelected: (selected) => {
this.modalsManager.setOption('selected', selected);
},
confirm: (modal) => {
modal.startLoading();
const stores = modal.getOption('selected');
const allStores = modal.getOption('stores');
const remove = allStores.filter((store) => !stores.includes(store)); // stores to be removed
return network.addStores(stores, remove).then(() => {
return this.hostRouter.refresh().then(() => {
this.notifications.success(this.intl.t('storefront.networks.index.network.stores.network-stores-update'));
});
});
},
});
});
}
/**
* Displays a confirmation modal to remove a specified store from the network.
* Allows the user to confirm the removal.
*
* @action
* @param {StoreModel} store - The store object to be removed.
*/
async removeStore(store) {
this.modalsManager.confirm({
title: this.intl.t('storefront.networks.index.network.stores.remove-this-store', { storeName: store.name, networkName: this.network.name }),
body: this.intl.t('storefront.networks.index.network.stores.longer-findable-by-this-network'),
acceptButtonIcon: 'check',
acceptButtonIconPrefix: 'fas',
declineButtonIcon: 'times',
declineButtonIconPrefix: 'fas',
confirm: (modal) => {
modal.startLoading();
this.fetch
.post(`networks/${this.network.id}/remove-stores`, { stores: [store.id] }, { namespace: 'storefront/int/v1' })
.then(() => {
this.stores.removeObject(store);
modal.done();
})
.catch((error) => {
modal.stopLoading();
this.notifications.serverError(error);
});
},
});
}
/**
* Displays a modal to view the details of a specified store.
* The modal includes the store's name and a "Done" button.
*
* @action
* @param {StoreModel} store - The store object whose details are to be viewed.
* @param {Object} [options={}] - Additional options for the modal.
*/
viewStoreDetails(store, options = {}) {
this.modalsManager.show('modals/store-details', {
title: this.intl.t('storefront.networks.index.network.stores.viewing-storefront', { storeName: store.name }),
acceptButtonText: this.intl.t('storefront.networks.index.network.stores.done'),
hideDeclineButton: true,
store,
...options,
});
}
/**
* Displays a modal to edit a specified store's details.
* Allows the user to make changes and confirm to save them.
*
* @action
* @param {StoreModel} store - The store object to be edited.
* @param {Object} [options={}] - Additional options for the modal.
*/
editStore(store, options = {}) {
this.modalsManager.show('modals/store-form', {
title: this.intl.t('storefront.networks.index.network.stores.editing-storefront', { storeName: store.name }),
acceptButtonText: this.intl.t('storefront.networks.index.network.stores.save-change'),
hideDeclineButton: true,
store,
confirm: (modal) => {
modal.startLoading();
return store
.save()
.then(() => {
this.notifications.success(`Changes to ${store.name} saved.`);
})
.catch((error) => {
console.error(error);
this.notifications.serverError(error);
});
},
...options,
});
}
/**
* Displays a modal to invite stores to the network via shareable link or email invitations.
* Allows the user to add or remove recipients, toggle the shareable link, and confirm the invitations.
*
* @action
*/
invite() {
const shareableLink = createShareableLink(`join/network/${this.network.public_id}`);
this.modalsManager.show('modals/share-network', {
title: this.intl.t('storefront.networks.index.network.stores.add-stores-to-network'),
acceptButtonText: this.intl.t('storefront.networks.index.network.stores.send-invitations'),
acceptButtonIcon: 'paper-plane',
acceptButtonDisabled: true,
shareableLink,
recipients: [],
network: this.network,
addRecipient: (email) => {
const recipients = this.modalsManager.getOption('recipients');
recipients.pushObject(email);
if (recipients.length === 0) {
this.modalsManager.setOption('acceptButtonDisabled', true);
} else {
this.modalsManager.setOption('acceptButtonDisabled', false);
}
},
removeRecipient: (index) => {
const recipients = this.modalsManager.getOption('recipients');
recipients.removeAt(index);
if (recipients.length === 0) {
this.modalsManager.setOption('acceptButtonDisabled', true);
} else {
this.modalsManager.setOption('acceptButtonDisabled', false);
}
},
toggleShareableLink: (enabled) => {
set(this.network, 'options.shareable_link_enabled', enabled);
this.network.save();
},
confirm: (modal) => {
modal.startLoading();
const recipients = modal.getOption('recipients');
const isValid = recipients.every((email) => isEmail(email));
if (!isValid) {
modal.stopLoading();
return this.notifications.error(this.intl.t('storefront.networks.index.network.stores.invalid-emails-provided-error'));
}
return this.network.sendInvites(recipients).then(() => {
modal.stopLoading();
this.notifications.success(this.intl.t('storefront.networks.index.network.stores.invitation-sent-recipients'));
});
},
});
}
}