Merge branch 'testing' into feature/3096-testing_selenium

This commit is contained in:
Cayo Puigdefabregas 2022-05-24 16:53:12 +02:00
commit f6e115909f
8 changed files with 761 additions and 452 deletions

View file

@ -28,7 +28,7 @@
"class-methods-use-this": "off", "class-methods-use-this": "off",
"eqeqeq": "warn", "eqeqeq": "warn",
"radix": "warn", "radix": "warn",
"max-classes-per-file": ["error", 2] "max-classes-per-file": "warn"
}, },
"globals": { "globals": {
"API_URLS": true, "API_URLS": true,

View file

@ -8,6 +8,7 @@ ml).
## master ## master
## testing ## testing
- [added] #273 Allow search/filter lots on lots management component.
## [2.1.1] - 2022-05-11 ## [2.1.1] - 2022-05-11
Hot fix release. Hot fix release.

View file

@ -5,14 +5,11 @@ Revises: eca457d8b2a4
Create Date: 2021-03-15 17:40:34.410408 Create Date: 2021-03-15 17:40:34.410408
""" """
import sqlalchemy as sa
import citext import citext
import teal import sqlalchemy as sa
from alembic import op from alembic import context, op
from alembic import context
from sqlalchemy.dialects import postgresql from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '51439cf24be8' revision = '51439cf24be8'
down_revision = '21afd375a654' down_revision = '21afd375a654'
@ -36,21 +33,126 @@ def upgrade_data():
def upgrade(): def upgrade():
## Trade # Trade
currency = sa.Enum('AFN', 'ARS', 'AWG', 'AUD', 'AZN', 'BSD', 'BBD', 'BDT', 'BYR', 'BZD', 'BMD', currency = sa.Enum(
'BOB', 'BAM', 'BWP', 'BGN', 'BRL', 'BND', 'KHR', 'CAD', 'KYD', 'CLP', 'CNY', 'AFN',
'COP', 'CRC', 'HRK', 'CUP', 'CZK', 'DKK', 'DOP', 'XCD', 'EGP', 'SVC', 'EEK', 'ARS',
'EUR', 'FKP', 'FJD', 'GHC', 'GIP', 'GTQ', 'GGP', 'GYD', 'HNL', 'HKD', 'HUF', 'AWG',
'ISK', 'INR', 'IDR', 'IRR', 'IMP', 'ILS', 'JMD', 'JPY', 'JEP', 'KZT', 'KPW', 'AUD',
'KRW', 'KGS', 'LAK', 'LVL', 'LBP', 'LRD', 'LTL', 'MKD', 'MYR', 'MUR', 'MXN', 'AZN',
'MNT', 'MZN', 'NAD', 'NPR', 'ANG', 'NZD', 'NIO', 'NGN', 'NOK', 'OMR', 'PKR', 'BSD',
'PAB', 'PYG', 'PEN', 'PHP', 'PLN', 'QAR', 'RON', 'RUB', 'SHP', 'SAR', 'RSD', 'BBD',
'SCR', 'SGD', 'SBD', 'SOS', 'ZAR', 'LKR', 'SEK', 'CHF', 'SRD', 'SYP', 'TWD', 'BDT',
'THB', 'TTD', 'TRY', 'TRL', 'TVD', 'UAH', 'GBP', 'USD', 'UYU', 'UZS', 'VEF', name='currency', create_type=False, checkfirst=True, schema=f'{get_inv()}') 'BYR',
'BZD',
'BMD',
'BOB',
'BAM',
'BWP',
'BGN',
'BRL',
'BND',
'KHR',
'CAD',
'KYD',
'CLP',
'CNY',
'COP',
'CRC',
'HRK',
'CUP',
'CZK',
'DKK',
'DOP',
'XCD',
'EGP',
'SVC',
'EEK',
'EUR',
'FKP',
'FJD',
'GHC',
'GIP',
'GTQ',
'GGP',
'GYD',
'HNL',
'HKD',
'HUF',
'ISK',
'INR',
'IDR',
'IRR',
'IMP',
'ILS',
'JMD',
'JPY',
'JEP',
'KZT',
'KPW',
'KRW',
'KGS',
'LAK',
'LVL',
'LBP',
'LRD',
'LTL',
'MKD',
'MYR',
'MUR',
'MXN',
'MNT',
'MZN',
'NAD',
'NPR',
'ANG',
'NZD',
'NIO',
'NGN',
'NOK',
'OMR',
'PKR',
'PAB',
'PYG',
'PEN',
'PHP',
'PLN',
'QAR',
'RON',
'RUB',
'SHP',
'SAR',
'RSD',
'SCR',
'SGD',
'SBD',
'SOS',
'ZAR',
'LKR',
'SEK',
'CHF',
'SRD',
'SYP',
'TWD',
'THB',
'TTD',
'TRY',
'TRL',
'TVD',
'UAH',
'GBP',
'USD',
'UYU',
'UZS',
'VEF',
name='currency',
create_type=False,
checkfirst=True,
)
op.drop_table('trade', schema=f'{get_inv()}') op.drop_table('trade', schema=f'{get_inv()}')
op.create_table('trade', op.create_table(
'trade',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('price', sa.Float(decimal_return_scale=4), nullable=True), sa.Column('price', sa.Float(decimal_return_scale=4), nullable=True),
sa.Column('lot_id', postgresql.UUID(as_uuid=True), nullable=True), sa.Column('lot_id', postgresql.UUID(as_uuid=True), nullable=True),
@ -59,36 +161,69 @@ def upgrade():
sa.Column('user_to_id', postgresql.UUID(as_uuid=True), nullable=False), sa.Column('user_to_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('document_id', citext.CIText(), nullable=True), sa.Column('document_id', citext.CIText(), nullable=True),
sa.Column('confirm', sa.Boolean(), nullable=True), sa.Column('confirm', sa.Boolean(), nullable=True),
sa.Column('code', citext.CIText(), default='', nullable=True, sa.Column(
comment = "This code is used for traceability"), 'code',
sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ), citext.CIText(),
sa.ForeignKeyConstraint(['user_from_id'], ['common.user.id'], ), default='',
sa.ForeignKeyConstraint(['user_to_id'], ['common.user.id'], ), nullable=True,
sa.ForeignKeyConstraint(['lot_id'], [f'{get_inv()}.lot.id'], ), comment="This code is used for traceability",
),
sa.ForeignKeyConstraint(
['id'],
[f'{get_inv()}.action.id'],
),
sa.ForeignKeyConstraint(
['user_from_id'],
['common.user.id'],
),
sa.ForeignKeyConstraint(
['user_to_id'],
['common.user.id'],
),
sa.ForeignKeyConstraint(
['lot_id'],
[f'{get_inv()}.lot.id'],
),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}' schema=f'{get_inv()}',
) )
op.add_column("trade", sa.Column("currency", currency, nullable=False), schema=f'{get_inv()}') op.add_column(
"trade", sa.Column("currency", currency, nullable=False), schema=f'{get_inv()}'
)
op.create_table(
op.create_table('confirm', 'confirm',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False), sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('action_id', postgresql.UUID(as_uuid=True), nullable=False), sa.Column('action_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(
sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ), ['id'],
sa.ForeignKeyConstraint(['action_id'], [f'{get_inv()}.action.id'], ), [f'{get_inv()}.action.id'],
sa.ForeignKeyConstraint(['user_id'], ['common.user.id'], ), ),
sa.ForeignKeyConstraint(
['action_id'],
[f'{get_inv()}.action.id'],
),
sa.ForeignKeyConstraint(
['user_id'],
['common.user.id'],
),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}' schema=f'{get_inv()}',
) )
## User # User
op.add_column('user', sa.Column('active', sa.Boolean(), default=True, nullable=True), op.add_column(
schema='common') 'user',
op.add_column('user', sa.Column('phantom', sa.Boolean(), default=False, nullable=True), sa.Column('active', sa.Boolean(), default=True, nullable=True),
schema='common') schema='common',
)
op.add_column(
'user',
sa.Column('phantom', sa.Boolean(), default=False, nullable=True),
schema='common',
)
upgrade_data() upgrade_data()
@ -99,28 +234,57 @@ def upgrade():
def downgrade(): def downgrade():
op.drop_table('confirm', schema=f'{get_inv()}') op.drop_table('confirm', schema=f'{get_inv()}')
op.drop_table('trade', schema=f'{get_inv()}') op.drop_table('trade', schema=f'{get_inv()}')
op.create_table('trade', op.create_table(
sa.Column('shipping_date', sa.TIMESTAMP(timezone=True), nullable=True, 'trade',
sa.Column(
'shipping_date',
sa.TIMESTAMP(timezone=True),
nullable=True,
comment='When are the devices going to be ready \n \ comment='When are the devices going to be ready \n \
for shipping?\n '), for shipping?\n ',
sa.Column('invoice_number', citext.CIText(), nullable=True, ),
comment='The id of the invoice so they can be linked.'), sa.Column(
sa.Column('price_id', postgresql.UUID(as_uuid=True), nullable=True, 'invoice_number',
citext.CIText(),
nullable=True,
comment='The id of the invoice so they can be linked.',
),
sa.Column(
'price_id',
postgresql.UUID(as_uuid=True),
nullable=True,
comment='The price set for this trade. \n \ comment='The price set for this trade. \n \
If no price is set it is supposed that the trade was\n \ If no price is set it is supposed that the trade was\n \
not payed, usual in donations.\n '), not payed, usual in donations.\n ',
),
sa.Column('to_id', postgresql.UUID(as_uuid=True), nullable=False), sa.Column('to_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('confirms_id', postgresql.UUID(as_uuid=True), nullable=True, sa.Column(
'confirms_id',
postgresql.UUID(as_uuid=True),
nullable=True,
comment='An organize action that this association confirms. \ comment='An organize action that this association confirms. \
\n \n For example, a ``Sell`` or ``Rent``\n \ \n \n For example, a ``Sell`` or ``Rent``\n \
can confirm a ``Reserve`` action.\n '), can confirm a ``Reserve`` action.\n ',
),
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(['confirms_id'], [f'{get_inv()}.organize.id'], ), sa.ForeignKeyConstraint(
sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ), ['confirms_id'],
sa.ForeignKeyConstraint(['price_id'], [f'{get_inv()}.price.id'], ), [f'{get_inv()}.organize.id'],
sa.ForeignKeyConstraint(['to_id'], [f'{get_inv()}.agent.id'], ), ),
sa.ForeignKeyConstraint(
['id'],
[f'{get_inv()}.action.id'],
),
sa.ForeignKeyConstraint(
['price_id'],
[f'{get_inv()}.price.id'],
),
sa.ForeignKeyConstraint(
['to_id'],
[f'{get_inv()}.agent.id'],
),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}' schema=f'{get_inv()}',
) )
op.drop_column('user', 'active', schema='common') op.drop_column('user', 'active', schema='common')
op.drop_column('user', 'phantom', schema='common') op.drop_column('user', 'phantom', schema='common')

View file

@ -5,12 +5,10 @@ Revises: 3eb50297c365
Create Date: 2020-12-29 20:19:46.981207 Create Date: 2020-12-29 20:19:46.981207
""" """
from alembic import context
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
import teal import teal
from alembic import context, op
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = 'b4bd1538bad5' revision = 'b4bd1538bad5'
@ -25,33 +23,71 @@ def get_inv():
raise ValueError("Inventory value is not specified") raise ValueError("Inventory value is not specified")
return INV return INV
def upgrade(): def upgrade():
# op.execute("COMMIT")
op.execute("ALTER TYPE snapshotsoftware ADD VALUE 'WorkbenchDesktop'")
SOFTWARE = sa.Enum(
'Workbench',
'WorkbenchAndroid',
'AndroidApp',
'Web',
'DesktopApp',
'WorkbenchDesktop',
name='snapshotsoftware',
create_type=False,
checkfirst=True,
)
# Live action # Live action
op.drop_table('live', schema=f'{get_inv()}') op.drop_table('live', schema=f'{get_inv()}')
op.create_table('live', op.create_table(
'live',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('serial_number', sa.Unicode(), nullable=True, sa.Column(
comment='The serial number of the Hard Disk in lower case.'), 'serial_number',
sa.Unicode(),
nullable=True,
comment='The serial number of the Hard Disk in lower case.',
),
sa.Column('usage_time_hdd', sa.Interval(), nullable=True), sa.Column('usage_time_hdd', sa.Interval(), nullable=True),
sa.Column('snapshot_uuid', postgresql.UUID(as_uuid=True), nullable=False), sa.Column('snapshot_uuid', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('software_version', teal.db.StrictVersionType(length=32), nullable=False), sa.Column(
sa.Column('licence_version', teal.db.StrictVersionType(length=32), nullable=False), 'software_version', teal.db.StrictVersionType(length=32), nullable=False
sa.Column('software', sa.Enum('Workbench', 'WorkbenchAndroid', 'AndroidApp', 'Web', ),
'DesktopApp', 'WorkbenchDesktop', name='snapshotsoftware'), nullable=False), sa.Column(
sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ), 'licence_version', teal.db.StrictVersionType(length=32), nullable=False
),
sa.Column('software', SOFTWARE, nullable=False),
sa.ForeignKeyConstraint(
['id'],
[f'{get_inv()}.action.id'],
),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}' schema=f'{get_inv()}',
) )
def downgrade(): def downgrade():
op.drop_table('live', schema=f'{get_inv()}') op.drop_table('live', schema=f'{get_inv()}')
op.create_table('live', op.create_table(
'live',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('serial_number', sa.Unicode(), nullable=True, sa.Column(
comment='The serial number of the Hard Disk in lower case.'), 'serial_number',
sa.Unicode(),
nullable=True,
comment='The serial number of the Hard Disk in lower case.',
),
sa.Column('usage_time_hdd', sa.Interval(), nullable=True), sa.Column('usage_time_hdd', sa.Interval(), nullable=True),
sa.Column('snapshot_uuid', postgresql.UUID(as_uuid=True), nullable=False), sa.Column('snapshot_uuid', postgresql.UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ), sa.ForeignKeyConstraint(
['id'],
[f'{get_inv()}.action.id'],
),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}' schema=f'{get_inv()}',
)
op.execute(
"select e.enumlabel FROM pg_enum e JOIN pg_type t ON e.enumtypid = t.oid WHERE t.typname = 'snapshotsoftware'"
) )

View file

@ -218,9 +218,13 @@
/** /**
* Avoid hide dropdown when user clicked inside * Avoid hide dropdown when user clicked inside
*/ */
document.getElementById("dropDownLotsSelector").addEventListener("click", event => { const dropdownLotSelector = document.getElementById("dropDownLotsSelector")
if (dropdownLotSelector != null) { // If exists selector it will set click event
dropdownLotSelector.addEventListener("click", event => {
event.stopPropagation(); event.stopPropagation();
}) })
}
/** /**
* Search form functionality * Search form functionality

View file

@ -1,5 +1,7 @@
"use strict"; "use strict";
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _classStaticPrivateFieldSpecGet(receiver, classConstructor, descriptor) { _classCheckPrivateStaticAccess(receiver, classConstructor); _classCheckPrivateStaticFieldDescriptor(descriptor, "get"); return _classApplyDescriptorGet(receiver, descriptor); } function _classStaticPrivateFieldSpecGet(receiver, classConstructor, descriptor) { _classCheckPrivateStaticAccess(receiver, classConstructor); _classCheckPrivateStaticFieldDescriptor(descriptor, "get"); return _classApplyDescriptorGet(receiver, descriptor); }
function _classCheckPrivateStaticFieldDescriptor(descriptor, action) { if (descriptor === undefined) { throw new TypeError("attempted to " + action + " private static field before its declaration"); } } function _classCheckPrivateStaticFieldDescriptor(descriptor, action) { if (descriptor === undefined) { throw new TypeError("attempted to " + action + " private static field before its declaration"); } }
@ -328,11 +330,59 @@ function export_file(type_file) {
$("#exportAlertModal").click(); $("#exportAlertModal").click();
} }
} }
class lotsSearcher {
static enable() {
if (this.lotsSearchElement) this.lotsSearchElement.disabled = false;
}
static disable() {
if (this.lotsSearchElement) this.lotsSearchElement.disabled = true;
}
/**
* do search when lot change in the search input
*/
static doSearch(inputSearch) {
const lots = this.getListLots();
for (let i = 0; i < lots.length; i++) {
if (lot.innerText.toLowerCase().includes(inputSearch.toLowerCase())) {
lot.parentElement.style.display = "";
} else {
lot.parentElement.style.display = "none";
}
}
}
}
_defineProperty(lotsSearcher, "lots", []);
_defineProperty(lotsSearcher, "lotsSearchElement", null);
_defineProperty(lotsSearcher, "getListLots", () => {
let lotsList = document.getElementById("LotsSelector");
if (lotsList) {
// Apply filter to get only labels
return Array.from(lotsList.children).filter(item => item.querySelector("label"));
}
return [];
});
document.addEventListener("DOMContentLoaded", () => {
lotsSearcher.lotsSearchElement = document.getElementById("lots-search");
lotsSearcher.lotsSearchElement.addEventListener("input", e => {
lotsSearcher.doSearch(e.target.value);
});
});
/** /**
* Reactive lots button * Reactive lots button
*/ */
async function processSelectedDevices() { async function processSelectedDevices() {
class Actions { class Actions {
constructor() { constructor() {
@ -584,6 +634,7 @@ async function processSelectedDevices() {
document.getElementById("ApplyDeviceLots").classList.add("disabled"); document.getElementById("ApplyDeviceLots").classList.add("disabled");
try { try {
lotsSearcher.disable();
listHTML.html("<li style=\"text-align: center\"><div class=\"spinner-border text-info\" style=\"margin: auto\" role=\"status\"></div></li>"); listHTML.html("<li style=\"text-align: center\"><div class=\"spinner-border text-info\" style=\"margin: auto\" role=\"status\"></div></li>");
const selectedDevices = await Api.get_devices(selectedDevicesID); const selectedDevices = await Api.get_devices(selectedDevicesID);
let lots = await Api.get_lots(); let lots = await Api.get_lots();
@ -614,6 +665,7 @@ async function processSelectedDevices() {
listHTML.html(""); listHTML.html("");
lotsList.forEach(lot => templateLot(lot, selectedDevices, listHTML, actions)); lotsList.forEach(lot => templateLot(lot, selectedDevices, listHTML, actions));
lotsSearcher.enable();
} catch (error) { } catch (error) {
console.log(error); console.log(error);
listHTML.html("<li style=\"color: red; text-align: center\">Error feching devices and lots<br>(see console for more details)</li>"); listHTML.html("<li style=\"color: red; text-align: center\">Error feching devices and lots<br>(see console for more details)</li>");

View file

@ -317,6 +317,47 @@ function export_file(type_file) {
} }
} }
class lotsSearcher {
static lots = [];
static lotsSearchElement = null;
static getListLots = () => {
const lotsList = document.getElementById("LotsSelector")
if (lotsList) {
// Apply filter to get only labels
return Array.from(lotsList.children).filter(item => item.querySelector("label"));
}
return [];
}
static enable() {
if (this.lotsSearchElement) this.lotsSearchElement.disabled = false;
}
static disable() {
if (this.lotsSearchElement) this.lotsSearchElement.disabled = true;
}
/**
* do search when lot change in the search input
*/
static doSearch(inputSearch) {
const lots = this.getListLots();
for (let i = 0; i < lots.length; i++) {
if (lot.innerText.toLowerCase().includes(inputSearch.toLowerCase())) {
lot.parentElement.style.display = "";
} else {
lot.parentElement.style.display = "none";
}
}
}
}
document.addEventListener("DOMContentLoaded", () => {
lotsSearcher.lotsSearchElement = document.getElementById("lots-search");
lotsSearcher.lotsSearchElement.addEventListener("input", (e) => { lotsSearcher.doSearch(e.target.value) })
})
/** /**
* Reactive lots button * Reactive lots button
@ -557,6 +598,7 @@ async function processSelectedDevices() {
document.getElementById("ApplyDeviceLots").classList.add("disabled"); document.getElementById("ApplyDeviceLots").classList.add("disabled");
try { try {
lotsSearcher.disable()
listHTML.html("<li style=\"text-align: center\"><div class=\"spinner-border text-info\" style=\"margin: auto\" role=\"status\"></div></li>") listHTML.html("<li style=\"text-align: center\"><div class=\"spinner-border text-info\" style=\"margin: auto\" role=\"status\"></div></li>")
const selectedDevices = await Api.get_devices(selectedDevicesID); const selectedDevices = await Api.get_devices(selectedDevicesID);
let lots = await Api.get_lots(); let lots = await Api.get_lots();
@ -589,6 +631,7 @@ async function processSelectedDevices() {
listHTML.html(""); listHTML.html("");
lotsList.forEach(lot => templateLot(lot, selectedDevices, listHTML, actions)); lotsList.forEach(lot => templateLot(lot, selectedDevices, listHTML, actions));
lotsSearcher.enable();
} catch (error) { } catch (error) {
console.log(error); console.log(error);
listHTML.html("<li style=\"color: red; text-align: center\">Error feching devices and lots<br>(see console for more details)</li>"); listHTML.html("<li style=\"color: red; text-align: center\">Error feching devices and lots<br>(see console for more details)</li>");

View file

@ -87,7 +87,16 @@
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<span class="d-none" id="activeTradeModal" data-bs-toggle="modal" data-bs-target="#tradeLotModal"></span> <span class="d-none" id="activeTradeModal" data-bs-toggle="modal" data-bs-target="#tradeLotModal"></span>
<ul class="dropdown-menu" aria-labelledby="btnLots" id="dropDownLotsSelector"> <ul class="dropdown-menu" aria-labelledby="btnLots" id="dropDownLotsSelector">
<div class="row w-100">
<div class="input-group mb-3 mx-2">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon1"><i class="bi bi-search"></i></span>
</div>
<input type="text" class="form-control" id="lots-search" placeholder="search" aria-label="search" aria-describedby="basic-addon1">
</div>
</div>
<h6 class="dropdown-header">Select lots where to store the selected devices</h6> <h6 class="dropdown-header">Select lots where to store the selected devices</h6>
<ul class="mx-3" id="LotsSelector"></ul> <ul class="mx-3" id="LotsSelector"></ul>
<li><hr /></li> <li><hr /></li>