Problem during adding elements in Flutter Hive - database

I'm facing some problems when using Hive in Flutter for the first time. Before that, I was using Sqflite, and things just performed as expected, but I switched to Hive to look more performant I switch to Hive.
The problem is that when I initialize two Hive boxes "AvatarsPack" and "AvatarItem" with data, even awaiting for each insertion on DB, it looks like Hive returns before all insertions been done, so my FutureBuilder builds without getting the values. I'm trying to solve this problem for about 3 days and I almost switching back to Sqflite, but I remember that there are amazing guys right here that would solve this issue in seconds, so it's what I'm doing, I'm asking all you for help. Thank you by reading until here, below it's the code:
avatars_view.dart
final _controller = AvatarsController();
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _controller.initializeRepository(),
builder: (context, snapshot) {
Future.delayed(Duration(seconds: 1)).then((value) => print(_controller.getAvatarsPacks()[9].avatars));
if (snapshot.connectionState == ConnectionState.done) {
print('#####${_controller.getAvatarsPacks()[0].avatars}');
return MyShop(
getOwnedItems: _controller.getOwnedAvatars,
ownedTabTile: 'Owned',
isScrollable: true,
headers: _controller.getHeaders(),
bodies:
_controller.getAvatarsPacks().map((e) => e.avatars).toList(),
);
}
return Container();
});
}
}
avatars_controller.dart
final _repository = AvatarsRepository();
final Map<String, int> _AVATARS_PACKS_AMOUNT = {
'superheroes': 50,
'children': 30,
'halloween-1': 50,
'halloween-2': 50,
'halloween-3': 50,
'hospital': 18,
'people-1': 50,
'people-2': 50,
'people-3': 18,
'professions': 50,
};
List<AvatarsPack> getAvatarsPacks() {
return _repository.getAllAvatarsPacks();
}
Future<void> initializeRepository() async {
await _repository
.openBoxes()
.then((value) => print('----Boxes are oppened'));
await _clearBoxes().then((value) => print('----Boxes were cleared'));
if (_repository.isEmpty()) {
print('----boxes are empty');
await _initializeRepositoryData().then((value) =>
print('avatars quantity: ${_repository.getQuantityAvatarItems()}'));
}
return;
}
List<String> getHeaders() {
return _AVATARS_PACKS_AMOUNT.keys.toList();
}
List<AvatarItem> getOwnedAvatars() {
return _repository.getOwnedAvatars();
}
void updateAvatarItem(AvatarItem avatarItem) =>
_repository.updateAvatarItem(avatarItem);
Future<void> _initializeRepositoryData() async {
print('*******_initializeRepositoryData called()');
return _AVATARS_PACKS_AMOUNT.forEach((key, quantity) async {
final avatarsPack = AvatarsPack.create(
packID: key,
quantity: quantity,
packItemPrice: Money.all(coins: 50, diamonds: 5));
await _repository.addPack(avatarsPack);
});
}
Future<void> _clearBoxes() {
return _repository.clearBoxes();
}
#override
FutureOr onClose() {
_repository.closeBoxes();
super.onClose();
}
}
avatars_repository.dart
Box _avatarsPackBox;
Box _avatarItemBox;
final AVATAR_PACK = 'avatarsPack';
final AVATAR_ITEM = 'avatarItem';
Box get _avatarsPackInstance {
return _avatarsPackBox ??= Hive.box(AVATAR_PACK);
}
Box get _avatarItemInstance {
return _avatarItemBox ??= Hive.box(AVATAR_ITEM);
}
Future<void> addPack(AvatarsPack avatarsPack) async {
await _avatarsPackInstance.add(avatarsPack.toJson());
return avatarsPack.avatars.forEach((element) async {
await _avatarItemInstance.add(element.toJson());
});
}
List<AvatarsPack> getAllAvatarsPacks() {
final avatarsPacks = _avatarsPackInstance.values
.map((json) => AvatarsPack.fromJson(json))
.toList();
avatarsPacks.forEach((avatarPack) {
avatarPack.avatars = getAvatarItemsFromPack(avatarPack.packID);
});
return avatarsPacks;
}
Future<void> addAvatarItem(AvatarItem avatarItem) async {
await _avatarItemInstance.add(avatarItem.toJson());
return;
}
Future<void> updateAvatarItem(AvatarItem avatarItem) {
final index =
getAllAvatarItems().indexWhere((e) => e.pack == avatarItem.pack);
return _avatarItemInstance.putAt(index, avatarItem.toJson());
}
List<AvatarItem> getAllAvatarItems() {
return _avatarItemInstance.values
.map((json) => AvatarItem.fromJson(json))
.toList();
}
int getQuantityAvatarItems() {
return _avatarItemInstance.values.length;
}
List<AvatarItem> getAvatarItemsFromPack(String packID) {
final List<AvatarItem> avatars = [];
getAllAvatarItems().forEach((avatarItem) {
if (avatarItem.pack == packID) avatars.add(avatarItem);
});
return avatars;
}
List<AvatarItem> getOwnedAvatars() {
return getAllAvatarItems().where((element) =>
element.isOwned()).toList();
}
Future<void> openBoxes() async {
await Hive.openBox(AVATAR_PACK);
await Hive.openBox(AVATAR_ITEM);
return;
}
Future<void> closeBoxes() async {
await _avatarsPackInstance.close();
await _avatarItemInstance.close();
return;
}
Future<void> clearBoxes() async {
await _avatarsPackInstance.clear();
await _avatarItemInstance.clear();
return;
}
bool isEmpty() {
return _avatarsPackInstance.isEmpty;
}
}
Concerning my avatars images, they are in folders which names are the same as their packID and each folder contains X elements named in this way img-0, img-2, ..., img-[x-1], img-x . Any feedback will be appreciated, thanks :)

Try using a ValueListenableBuilder() instead of the FutureBuilder().
This watches changes on the Box and reflects those accordingly.
import 'package:hive_flutter/hive_flutter.dart';
ValueListenableBuilder(
valueListenable:
Hive.box(YOURBOX).listenable(),
builder: (context, Box box, _) {
return SomeWidget(withSomeData);
},

Related

Not saving the flutter switch value to sqflite database

I am a completely a beginner to sqlite and flutter. I was trying to create a local database to my flutter to do app. So I watched some youtube videos and started to implementing a database using flutter sqflite plugin. Everything worked fine because all I did was copy typing the you tubers code, until I had to add an extra parameter to the code which is a boolean, in order to track the status of the task (like done the task or not). I used an int value to save the bool, while sqlite does not supports boolean values. I used two functions, one to update the text and the other to update the switch value.
And secondly when I tap on a switch it triggers all the switches in the list. I want to solve that issue as well.
Model class for the Task
class Tasksdb {
final int? id;
final String taskName;
bool isDone;
Tasksdb({
this.id,
required this.taskName,
required this.isDone,
});
factory Tasksdb.fromMap(Map<String, dynamic> json) => Tasksdb(
id: json['id'],
taskName: json['taskName'],
isDone: (json['isDone'] as int) == 0 ? false : true);
Map<String, dynamic> toMap() {
return {
'id': id,
'taskName': taskName,
'isDone': isDone,
};
}
}
DatabaseHelper class
class DatabaseHelper {
DatabaseHelper._privateConstructor();
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
static Database? _database;
Future<Database> get database async => _database ??= await _initDatabase();
Future<Database> _initDatabase() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, 'tasks.db');
return await openDatabase(
path,
version: 1,
onCreate: _onCreate,
);
}
Future _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE IF NOT EXISTS "taskstable" (
"id" INTEGER,
"taskName" TEXT,
"isDone" INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY("id" AUTOINCREMENT)
);
''');
}
Future<List<Tasksdb>> getTasks() async {
Database db = await instance.database;
var tasksQuery = await db.query(
'taskstable',
);
List<Tasksdb> taskList = tasksQuery.isNotEmpty
? tasksQuery.map((c) => Tasksdb.fromMap(c)).toList()
: [];
return taskList;
}
Future<int> add(Tasksdb task) async {
Database db = await instance.database;
return await db.insert('taskstable', {
'id': task.id,
'taskName': task.taskName,
'isDone': 0,
});
}
Future<int> update(Tasksdb task) async {
Database db = await instance.database;
return await db.update(
'taskstable',
task.toMap(),
where: "id = ?",
whereArgs: [task.id],
);
}
Future<int> updateIsDone(bool isDoneTodb) async {
Database db = await instance.database;
return await db.update(
'taskstable',
{
'isDone': isDoneTodb == true ? 1 : 0,
},
);
}
}
HomeScreen widget
class SqliteApp extends StatefulWidget {
const SqliteApp({Key? key}) : super(key: key);
#override
_SqliteAppState createState() => _SqliteAppState();
}
class _SqliteAppState extends State<SqliteApp> {
int? selectedId;
final textController = TextEditingController();
bool isDone = false;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: TextField(
controller: textController,
),
),
body: Center(
child: FutureBuilder<List<Tasksdb>>(
future: DatabaseHelper.instance.getTasks(),
builder: (BuildContext context,
AsyncSnapshot<List<Tasksdb>> snapshot) {
if (!snapshot.hasData) {
return const Center(child: Text('Loading...'));
}
return snapshot.data!.isEmpty
? const Center(child: Text('No tasks in List.'))
: ListView(
children: snapshot.data!.map((task) {
return Center(
child: Card(
color: selectedId == task.id
? Colors.green
: Colors.yellow,
child: ListTile(
trailing: Switch( //the problem is here, doesn't save to db
value: isDone,
onChanged: (val) async {
setState(() {
isDone = val;
});
await DatabaseHelper.instance
.updateIsDone(
isDone,
);
}),
title: Text(task.taskName),
onTap: () {
setState(() {
if (selectedId == null) {
textController.text = task.taskName;
selectedId = task.id;
} else {
textController.text = 'add something';
selectedId = null;
}
});
},
),
),
);
}).toList(),
);
}),
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
await DatabaseHelper.instance.add(
Tasksdb(
taskName: textController.text,
isDone: false,
),
);
setState(() {
textController.clear();
selectedId = null;
});
},
),
),
);
}
}
I found the answer: (in case of if someone had the same question)
I removed the bool isDone from the material app widget, and instead of assigning the switch val to that bool I assigned it to database's task.isDone value. To avoid switch's auto trigger, I parsed the Taskdb to the updateIsDone function
Future<int> updateIsDone(Tasksdb task, bool isDoneTodb) async {
Database db = await instance.database;
return await db.update(
'taskstable',
{
'isDone': isDoneTodb == true ? 1 : 0,
},
where: "id = ?",
whereArgs: [task.id]);
}
...
Switch(
value: task.isDone,
onChanged: (val) async {
setState(() {
task.isDone = val;
});
await DatabaseHelper.instance
.updateIsDone(task, task.isDone);
});

"Warning database has been locked" warning with SQFlite and code stops. Why can I not query the table?

I'm having trouble querying a SQFlite db table in Flutter. I get the following warning several times:
I/chatty (32047): uid=10160(com.example.SQFLite_test) 1.ui identical 18498 lines 2
I/flutter (32047): Warning database has been locked for 0:00:10.000000. Make sure you always use the transaction
object for database operations during a transaction
I get the warning when calling the getClients() method to get all clients from the Client table. The main issue though is that it seems to freeze the code as well. Even when I only try to select the top 100, it still gives me the warning and it freezes and doesn't progress.
My db class helping me to init and manage the database:
class LogServiceTwo {
LogServiceTwo._();
static final LogServiceTwo logRepo = LogServiceTwo._();
static Database _database;
Future<Database> get database async {
if (_database != null) {
return _database;
}
var db = await openDb();
// if (db == null) {
// _database = await initDB();
// return _database;
// }
var hasClientTableB = await hasClientTable(db);
if (hasClientTableB) {
_database = db;
return _database;
}
// var backup = await restoreBackup(db);
// if (backup != null) {
// _database = backup;
// return _database;
// }
await createClientTable(db);
_database = db;
return _database;
}
Future createClientTable(Database db) async {
await db.execute("CREATE TABLE Client ("
"id INTEGER PRIMARY KEY,"
"first_name TEXT,"
"last_name TEXT,"
"blocked BIT"
")");
}
Future<bool> hasClientTable(Database db) async {
try {
var table = await db.query("Client");
return table != null;
} catch (e) {
return false;
}
}
Future<Database> openDb() async {
try {
var path = await getPersistentDbPath();
var db = await openDatabase(path, version: 1);
return db;
} catch (e) {
return null;
}
}
Future initDB() async {
var path = await getPersistentDbPath();
return await openDatabase(path, version: 1, onOpen: (db) {}, onCreate: (Database db, int version) async {
await db.execute("CREATE TABLE Client ("
"id INTEGER PRIMARY KEY,"
"first_name TEXT,"
"last_name TEXT,"
"blocked BIT"
")");
});
}
Future newClient(Client newClient) async {
final db = await database;
var res = await db.insert("Client", newClient.toMap());
return res;
}
Future newClients(List<Client> clients) async {
var clientMaps = clients.map((client) => client.toMap()).toList();
final db = await database;
clientMaps.forEach((clientMap) async {
await db.insert("Client", clientMap);
});
}
Future<Client> getClient(int id) async {
final db = await database;
var res = await db.query("Client", where: "id = ?", whereArgs: [id]);
return res.isNotEmpty ? Client.fromMap(res.first) : Null;
}
Future<List<Client>> getAllClients() async {
final db = await database;
var res = await db.query("Client");
List<Client> list = res.isNotEmpty ? res.map((c) => Client.fromMap(c)).toList() : [];
return list;
}
Future<List<Client>> getBlockedClients() async {
final db = await logRepo.database;
var res = await db.rawQuery("SELECT * FROM Client WHERE blocked=1");
List<Client> list = res.isNotEmpty ? res.toList().map((c) => Client.fromMap(c)) : null;
return list;
}
Future<List<String>> getTables() async {
var db = await logRepo.database;
var tableNames = (await db.query('sqlite_master', where: 'type = ?', whereArgs: ['table'])).map((row) => row['name'] as String).toList(growable: false);
return tableNames;
}
Future<String> getPersistentDbPath() async {
return await createPersistentDbDirecotry();
}
Future createPersistentDbDirecotry() async {
var externalDirectoryPath = await ExtStorage.getExternalStorageDirectory();
var persistentDirectory = "$externalDirectoryPath/db_persistent";
await createDirectory(persistentDirectory);
return "$persistentDirectory/persistent.db";
}
Future createDirectory(String path) async {
await (new Directory(path).create());
}
Future<bool> askForWritePermission() async {
var status = await Permission.storage.status;
if (!status.isGranted) {
status = await Permission.storage.request();
return status.isGranted;
}
return status.isGranted;
}
Future mockData() async {
var clients = ClientMocker.createClients();
await newClients(clients);
}
Future deleteAll() async {
var db = await database;
await db.rawDelete("DELETE FROM Client");
}
// Get all clients, throws warnings and stops proceeding in the code.
Future getClients() async {
try {
var db = await database;
return await db.rawQuery("SELECT * FROM Client");
} catch (e) {
print(e);
}
}
}
My main class calling the database service for testing purposes:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
// This makes the visual density adapt to the platform that you run
// the app on. For desktop platforms, the controls will be smaller and
// closer together (more dense) than on mobile platforms.
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
void initState() {
// firstTest();
// secondTest();
thirdTest();
testText = "";
super.initState();
}
String testText;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
children: [
Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Text(testText),
)
],
),
),
);
}
Future firstTest() async {
testText = "";
// var client = new Client(blocked: false, firstName: "Tobias", lastName: "Eliasson", id: null);
// await LogRepository.logRepo.newClient(client);
// await LogRepository.logRepo.newClient(client);
// await LogRepository.logRepo.newClient(client);
var clients = await LogRepository.logRepo.getAllClients();
clients.forEach((c) {
setState(() {
testText += "\n${c.toMap()}";
});
print(c.toMap());
});
setState(() {
testText += "Length of found clients: ${clients.length.toString()}";
});
var success = await LogRepository.logRepo.saveBackup();
print(success);
}
Future secondTest() async {
try {
await LogRepository.logRepo.deleteAll();
await LogRepository.logRepo.mockData();
var a = DateTime.now();
print("Saving backup $a");
var backupSuccess = await LogRepository.logRepo.saveBackup();
print("Backup success: $backupSuccess");
var b = DateTime.now();
print("Saved backup:${a.difference(b)}");
} catch (e) {
print("Error!!!");
print(e);
}
}
Future thirdTest() async {
await LogServiceTwo.logRepo.database;
await LogServiceTwo.logRepo.mockData();
var clients = await LogServiceTwo.logRepo.getClients();
print(clients.length);
}
}
As far as I can see, I await all db operations and only use on db object to access it so there shouldn't be any weird parallell access going on. Maybe you find an error somewhere I'm missing though. In case you wonder why I create the database in external memory, is that the database needs to be persistent and saved when uninstalling the app or updating it.
Thanks!
I found the problem and learnt a lesson. In short, follow conventions and best practices when accessing a database. The problem was that I inserted one client at the time inside the method:
Future newClients(List<Client> clients) async {
var clientMaps = clients.map((client) => client.toMap()).toList();
final db = await database;
clientMaps.forEach((clientMap) async {
await db.insert("Client", clientMap);
});
}
When inserting or doing many db operations at the same moment, use batch and commit instead like this:
Future newClients(List<Client> clients) async {
var clientMaps = clients.map((client) => client.toMap()).toList();
final db = await database;
var batch = db.batch();
clientMaps.forEach((clientMap) async {
batch.insert("Client", clientMap);
});
await batch.commit(noResult: true);
}
With the batch and commit solution I could insert 90 000 clients on first try with no errors and could query them all after.

how to get the result from recursive promises in a redux action

I've searched the net, and I can't find out a solution. My final goal is to pull all the data from a dynamodb table. The problem is when a table is bigger than 1MB, in the response I'll get one chunk of data and a LastEvaluatedKey parameter (which provides the index I can use in the next call to get the next chunk). The scan operation is documented here if needed.
I'm using reactjs, redux and redux-thunk in my app.
I have used promises moderately in the single or chained formats, but this one is more challenging that I could resolve so far. What puzzles me is the fact that the new calls can not be made without receiving the previous response, so the calls can not be done simultaneously in my opinion. In another hand since the scan operation is a promise (as far as I understand) if I try to return a promise from my own method the action does not receive the results.
I'm very confused and I really like to understand how I can get this to work.
action:
function getDynamodbTableRecords(tableName) {
return dispatch => {
dispatch(request());
var recordsSet = [];
var data = myAwsService.getTableRecords(tableName, null) || {Items:[]};
if (data.Items.length > 0){
data.Items.map(record => {
recordsSet.push(record);
});
dispatch(success(recordsSet));
} else {
dispatch(failure("No Records Found!"));
}
};
function request() { return { type: DATA_LOADING, selectedTable: tableName } }
function success(tableRecords) { return { type: DATA_LOAD_SUCCESS, tableRecords } }
function failure(error) { return { type: DATA_LOAD_FAILED, errors: error } }
}
myAwsService:
function getTableRecords(tableName, lastEvaluatedKey = null) {
getRecordsBatch(tableName, lastEvaluatedKey)
.then(
data => {
if (data.LastEvaluatedKey) {
return getTableRecords(tableName, data.LastEvaluatedKey)
.then(
nextData => {
data.Items = data.Items.concat(nextData.Items);
}
)
}
return data;
}
)
}
function getRecordsBatch(tableName, lastEvaluatedKey = null) {
var awsDynamodb = new DynamoDB();
let params = { TableName: tableName };
if (lastEvaluatedKey) {
params['ExclusiveStartKey'] = lastEvaluatedKey;
}
return new Promise((resolve, reject) => {
awsDynamodb.scan(params, function(err, data) {
if (err) {
reject(err);
}
return resolve(data);
});
});
}
Not sure if your recursive promise is working but I'd do it like this:
function getTableRecords(
tableName,
lastEvaluatedKey = null,
result = { Items: [] }
) {
return getRecordsBatch(tableName, lastEvaluatedKey).then(
data => {
if (data.LastEvaluatedKey) {
return getTableRecords(
tableName,
data.LastEvaluatedKey,
{
...data,
Items: result.Items.concat(data.Items),
}
);
}
return {
...data,
Items: result.Items.concat(data.Items),
};
}
);
}
The action should also dispatch the data.Items and not the promise that getTabelRecords returns and you probably want to dispatch failure action if something goes wrong:
function getDynamodbTableRecords(tableName) {
return async dispatch => {
dispatch(request());
//you probably want the data, not a promise of data
try {
var data = await myAwsService.getTableRecords(
tableName,
null
);
if (data.Items.length > 0) {
//no reason to have the temporary recordSet variable
dispatch(success(data.Items.map(record => record)));
} else {
dispatch(failure('No Records Found!'));
}
} catch (e) {
dispatch(failure(e.message));
}
};
function request() {
return { type: DATA_LOADING, selectedTable: tableName };
}
function success(tableRecords) {
return { type: DATA_LOAD_SUCCESS, tableRecords };
}
function failure(error) {
return { type: DATA_LOAD_FAILED, errors: error };
}
}

store.getRootField(...) returns null

I call an api to add a new request:
import { commitMutation, graphql } from "react-relay";
import { REQUEST_STATUS_NEW } from "../../../constants";
const mutation = graphql`
mutation CreateRequestMutation($input: CreateRequestInput!) {
createRequest(input: $input) {
request {
id
tid
title
description
price
commission
value
expirationDate
createdAt
completionDate
multipleResponders
draft
status
requestProposals
type {
id
name
}
industry {
id
name
}
applications {
id
}
myApplication {
id
}
}
}
}
`;
let tempId = 0;
function sharedUpdater(store, request) {
const root = store.getRoot();
const newRequests = root
.getLinkedRecords("requests", { own: true })
.filter(r => r);
if (!newRequests.find(m => m.getValue("id") === request.getValue("id"))) {
newRequests.push(request);
}
root.setLinkedRecords(newRequests, "requests", { own: true });
}
export const commit = (environment, input) => {
tempId += 1;
return commitMutation(environment, {
mutation,
variables: { input },
updater: store => {
const payload = store.getRootField("createRequest");
console.log('payload: ', payload)
const request = payload.getLinkedRecord("request");
sharedUpdater(store, request);
}
});
};
But each time I call it, store.getRootField return me null. I cant really understand where is the problem where I can investigate further. Any help appreciated. Seems like server doesnt have any issues at their side. How can I debug this?

Using SP.Modal dialog in SPFX command set

My requirement is to open a SharePoint page in a modal dialogue using command set in a list. I have followed this:
MSDN tutorial to create command set
and this question:
How to refernence sp.js
This is my .ts file code
import { override } from '#microsoft/decorators';
import { Log } from '#microsoft/sp-core-library';
import {
BaseListViewCommandSet,
Command,
IListViewCommandSetListViewUpdatedParameters,
IListViewCommandSetExecuteEventParameters
} from '#microsoft/sp-listview-extensibility';
import { Dialog } from '#microsoft/sp-dialog';
import { SPComponentLoader } from '#microsoft/sp-loader';
import * as strings from 'DocManagerCommandSetStrings';
require('sp-init');
require('microsoft-ajax');
require('sp-runtime');
require('sharepoint');
/**
* If your command set uses the ClientSideComponentProperties JSON input,
* it will be deserialized into the BaseExtension.properties object.
* You can define an interface to describe it.
*/
export interface IDocManagerCommandSetProperties {
// This is an example; replace with your own properties
sampleTextOne: string;
sampleTextTwo: string;
}
const LOG_SOURCE: string = 'DocManagerCommandSet';
export default class DocManagerCommandSet extends BaseListViewCommandSet<IDocManagerCommandSetProperties> {
#override
public onInit(): Promise<void> {
Log.info(LOG_SOURCE, 'Initialized DocManagerCommandSet');
return Promise.resolve();
}
#override
public onListViewUpdated(event: IListViewCommandSetListViewUpdatedParameters): void {
const compareOneCommand: Command = this.tryGetCommand('COMMAND_1');
if (compareOneCommand) {
// This command should be hidden unless exactly one row is selected.
compareOneCommand.visible = event.selectedRows.length === 1;
}
}
#override
public onExecute(event: IListViewCommandSetExecuteEventParameters): void {
switch (event.itemId) {
case 'COMMAND_1':
Dialog.alert(`${this.properties.sampleTextOne}`);
break;
case 'COMMAND_2':
//DocManagerCommandSet._loadSPJSOMScripts();
var options = {
title: "My Dialog Title",
width: 400,
height: 600,
url: "/_layouts/DialogPage.aspx" };
var value = SP.UI.ModalDialog.showModalDialog(options);
// Dialog.alert(`${this.properties.sampleTextTwo}`);
break;
default:
throw new Error('Unknown command');
}
}
private static getSiteCollectionUrl(): string {
let baseUrl = window.location.protocol + "//" + window.location.host;
const pathname = window.location.pathname;
const siteCollectionDetector = "/sites/";
if (pathname.indexOf(siteCollectionDetector) >= 0) {
baseUrl += pathname.substring(0, pathname.indexOf("/", siteCollectionDetector.length));
}
return baseUrl;
}
private static _loadSPJSOMScripts() {
const siteColUrl = "https://shelldevelopment.sharepoint.com/sites/SPODA0332/";
SPComponentLoader.loadScript(siteColUrl + '/_layouts/15/init.js', {
globalExportsName: '$_global_init'
})
.then((): Promise<{}> => {
return SPComponentLoader.loadScript(siteColUrl + '/_layouts/15/MicrosoftAjax.js', {
globalExportsName: 'Sys'
});
})
.then((): Promise<{}> => {
return SPComponentLoader.loadScript(siteColUrl + '/_layouts/15/SP.Runtime.js', {
globalExportsName: 'SP'
});
})
.then((): Promise<{}> => {
return SPComponentLoader.loadScript(siteColUrl + '/_layouts/15/SP.js', {
globalExportsName: 'SP'
});
}) .then((): Promise<{}> => {
return SPComponentLoader.loadScript('/_layouts/15/sp.init.js', {
globalExportsName: 'SP'
});
}).then((): Promise<{}> => {
return SPComponentLoader.loadScript('/_layouts/15/sp.ui.dialog.js', {
globalExportsName: 'SP'
});
});
}
}
I am getting the following error:
cannot find the name 'SP'.in the line
SP.UI.ModalDialog.showModalDialog(options)
Kindly provide some insights as I am a beginner in SPFX
Theoretically you need to uncomment //DocManagerCommandSet._loadSPJSOMScripts(); and wait for the promise to return.
Update the loadSPJSOMScripts message to return the promise:
private static _loadSPJSOMScripts(): Promise<void> {
const siteColUrl = "https://shelldevelopment.sharepoint.com/sites/SPODA0332/";
return SPComponentLoader.loadScript(siteColUrl + '/_layouts/15/init.js', {
globalExportsName: '$_global_init'
})
// [the rest of the calls... ]
.then(_ => {});
}
to load in the onInit():
public onInit(): Promise<void> {
return Promise.resolve()
.then(_ => {
return DocManagerCommandSet._loadSPJSOMScripts();
});
}
Or in your onExecute:
#override
public onExecute(event: IListViewCommandSetExecuteEventParameters): void {
let launchModal = false;
switch (event.itemId) {
case 'COMMAND_1':
Dialog.alert(`${this.properties.sampleTextOne}`);
break;
case 'COMMAND_2':
launchModal = true;
break;
// ...
}
if (launchModal) {
DocManagerCommandSet._loadSPJSOMScripts()
.then(_ => {
var options = {
title: "My Dialog Title",
width: 400,
height: 600,
url: "/_layouts/DialogPage.aspx"
};
var value = SP.UI.ModalDialog.showModalDialog(options);
});
}
}
That being said, there may be better ways to work with JSOM in SPFX.

Resources