Why session.getSaveBatch() is undefined when child record was added - Ext 5.1.1 - session

Well the title says it all, details following.
I have two related models, User & Role.
User has roles defined as:
Ext.define('App.model.security.User', {
extend: 'App.model.Base',
entityName: 'User',
fields: [
{ name: 'id' },
{ name: 'email'},
{ name: 'name'},
{ name: 'enabled', type: 'bool'}
],
manyToMany: 'Role'
});
Then I have a grid of users and a form to edit user's data including his roles.
The thing is, when I try to add or delete a role from the user a later call to session.getSaveBatch() returns undefined and then I cannot start the batch to send the modifications to the server.
How can I solve this?

Well after reading a lot I found that Ext won't save the changed relationships between two models at least on 5.1.1.
I've had to workaround this by placing an aditional field on the left model (I named it isDirty) with a default value of false and set it true to force the session to send the update to the server with getSaveBatch.
Later I'll dig into the code to write an override to BatchVisitor or a custom BatchVisitor class that allow to save just associations automatically.
Note that this only occurs when you want to save just the association between the two models and if you also modify one of the involved entities then the association will be sent on the save batch.

Well this was interesting, I've learned a lot about Ext by solving this simple problem.
The solution I came across is to override the BatchVisitor class to make use of an event handler for the event onCleanRecord raised from the private method visitData of the Session class.
So for each record I look for left side entities in the matrix and if there is a change then I call the handler for onDirtyRecord which is defined on the BatchVisitor original class.
The code:
Ext.define('Ext.overrides.data.session.BatchVisitor', {
override: 'Ext.data.session.BatchVisitor',
onCleanRecord: function (record) {
var matrices = record.session.matrices
bucket = null,
ops = [],
recordId = record.id,
className = record.$className;
// Before anything I check that the record does not exists in the bucket
// If it exists then any change on matrices will be considered (so leave)
try {
bucket = this.map[record.$className];
ops.concat(bucket.create || [], bucket.destroy || [], bucket.update || []);
var found = ops.findIndex(function (element, index, array) {
if (element.id === recordId) {
return true;
}
});
if (found != -1) {
return;
}
}
catch (e) {
// Do nothing
}
// Now I look for changes on matrices
for (name in matrices) {
matrix = matrices[name].left;
if (className === matrix.role.cls.$className) {
slices = matrix.slices;
for (id in slices) {
slice = slices[id];
members = slice.members;
for (id2 in members) {
id1 = members[id2][0]; // This is left side id, right side is index 1
state = members[id2][2];
if (id1 !== recordId) { // Not left side => leave
break;
}
if (state) { // Association changed
this.onDirtyRecord(record);
// Same case as above now it exists in the bucket (so leave)
return;
}
}
}
}
}
}
});
It works very well for my needs, probably it wont be the best solution for others but can be a starting point anyways.
Finally, if it's not clear yet, what this does is give the method getSaveBatch the ability to detect changes on relationships.

Related

How to create Strapi component data in a lifecycle

I want to add content to a repeatable component in my beforeUpdate hook. (adding a changed slug to a “previous slugs” list)
in v3, I could just push new data on the component array and it would save.
in v4, it doesn’t work like that. Component data now holds __pivot: and such. I do not know how to add new data to this. I’ve tried adding a component with the entityService first, and adding that result to the array. It seemed to work, but it has strange behavior that the next saves puts in two entries. I feel like there should be an easier way to go about this.
It seems like the way to go about this is to create the pivot manually:
// create an entry for the component
const newRedirect = await strapi.entityService.create('redirects.redirect', {
data: {
from: oldData.slug,
},
});
// add the component to this model entry
data.redirects = [...data.redirects, {
id: newRedirect.id,
__pivot: { field: 'redirects', component_type: 'redirects.redirect' },
}];
But this feels pretty hacky. If I change the components name or the field key, this will break. I'd rather have a Strapi core way of doing this
the way strapi currently handles components is by providing full components array, so in case you want to inject something, you have to read components first and then apply full update, if it makes it clear.
Update
So after few hours of searching, had to do few hours of trail and error, however here is the solution, using knex:
module.exports = {
async beforeUpdate(event) {
// get previous slug
const { slug: previousSlug } = await strapi.db
.query("api::test.test")
.findOne({ where: event.params.where });
// create component
const [component] = await strapi.db
// this name of components table in database
.connection("components_components_previous_slugs")
.insert({ slug: previousSlug })
.returning("id");
// append component to event
event.params.data.previousSlugs = [
...event.params.data.previousSlugs,
{
id: component.id,
// the pivot, you have to copy manually
// 'field' is the name of the components property
// 'component_type' is internal name of component
__pivot: {
field: "previousSlugs",
component_type: "components.previous-slugs",
},
},
];
},
};
So, seems there is no service, or something exposed in strapi to create component for you.
The stuff that also required to be noted, on my first attempt i try to create relation manually in tests_components table, made for me after i added a repeatable component, to content-type, but after an hour more i found out that is WRONG and should not be done, seems strapi does that under the hood and modifying that table actually breaks logic...
so if there is more explanation needed, ping me here...
result:
You can update, create and delete component data that is attached to a record with Query Engine API, which is provided by Strapi.
To modify component data you just need the ID.
const { data } = event.params;
const newData = {
field1: value1,
etc...
};
await strapi.query('componentGroup.component').update({
where: { id: data.myField.id },
data: newData
})
When you have a component field that equals null you need to create that component and point to it.
const tempdata = await strapi.query('componentGroup.component').create(
{ data: newData }
);
data.myField = {
id: tempdata.id,
__pivot: {
field: 'myField',
component_type: 'componentGroup.component'
}
}
Se the Strapi forum for more information.

How to create a Data Table from forge viewer

I want to create a table that represents the data from a model that i have loaded in the forge viewer.
i want the table to be in a docking panel, and show the propertys for elements in the model ( level, name, comment,Area, type name--> for each element [if it has the property])
i have tried to use the API reference, and create a DataTable, but i did not find examples of how to actually impliment it.
where and when do i need to set the datatable? ( after or before creating the docking pannel?)
what is the content of the arrays that i should pass in the constructor? ( according to the documentation : array of arrays for the rows, and array for the columns. is the row array, is simply an array that contains the columns arrays?)
this is my current code for the extension that shows the amount to instances in the model for each property that i want to adjust:
'''
class testTest extends Autodesk.Viewing.Extension {
constructor(viewer, options) {
super(viewer, options);
this._group = null;
this._button = null;
}
load() {
console.log('testTest has been loaded');
return true;
}
unload() {
// Clean our UI elements if we added any
if (this._group) {
this._group.removeControl(this._button);
if (this._group.getNumberOfControls() === 0) {
this.viewer.toolbar.removeControl(this._group);
}
}
console.log('testTest has been unloaded');
return true;
}
// The Viewer contains all elements on the model, including categories (e.g. families or part definition),
//so we need to enumerate the leaf nodes, meaning actual instances on the model.
getAllLeafComponents(callback) {
this.viewer.getObjectTree(function (tree) {
let leaves = [];// an empty 'leaf' list that we want to fill wiith the objects that has no mo children
//dbId== object id
// for each child that we enumerate from a root, call a code , and finally a true /false flag parameter that run the function recursivly for all the children of a child.
tree.enumNodeChildren(tree.getRootId(), function (dbId) {
if (tree.getChildCount(dbId) === 0) {
leaves.push(dbId);// if the object has no children--> add it to the list.
}
}, true);// the last argument we past ("true") will make sure that the function in the seccond argument ("function (dbId)(...)" ")will run recursively not only for the children of the roots,
//but for all the children and childrtn's children.
callback(leaves);//return the leaves
});
}
onToolbarCreated() {
// Create a new toolbar group if it doesn't exist
this._group = this.viewer.toolbar.getControl('allMyAwesomeExtensionsToolbar');//if there is no controller named "allMyAwesomeExtensionsToolbar" create one
if (!this._group) {
this._group = new Autodesk.Viewing.UI.ControlGroup('allMyAwesomeExtensionsToolbar');
this.viewer.toolbar.addControl(this._group);// add the control to tool bar
}
// Add a new button to the toolbar group
this._button = new Autodesk.Viewing.UI.Button('testTest');
this._button.onClick = (ev) => {
// Check if the panel is created or not
if (this._panel == null) {//check if there is an instance of our pannel. if not- create one
this._panel = new ModelSummaryPanel(this.viewer, this.viewer.container, 'modelSummaryPanel', 'Model Summary');
}
// Show/hide docking panel
this._panel.setVisible(!this._panel.isVisible());//cal a method from the parent to show/ hide the panel -->use this to toggle from visible to invisible
this._panel.set
// If panel is NOT visible, exit the function
if (!this._panel.isVisible())
return;
// First, the viewer contains all elements on the model, including
// categories (e.g. families or part definition), so we need to enumerate
// the leaf nodes, meaning actual instances of the model. The following
// getAllLeafComponents function is defined at the bottom
this.getAllLeafComponents((dbIds) => {// now we have the list of the Id's of all the leaves
// Now for leaf components, let's get some properties and count occurrences of each value
debugger;
const filteredProps = ['Level','Name','Comments','Area','Type Name'];
// Get only the properties we need for the leaf dbIds
this.viewer.model.getBulkProperties(dbIds,filteredProps , (items) => {
// Iterate through the elements we found
items.forEach((item) => {
// and iterate through each property
item.properties.forEach(function (prop) {
// Use the filteredProps to store the count as a subarray
if (filteredProps[prop.displayName] === undefined)
filteredProps[prop.displayName] = {};
// Start counting: if first time finding it, set as 1, else +1
if (filteredProps[prop.displayName][prop.displayValue] === undefined)
filteredProps[prop.displayName][prop.displayValue] = 1;
else
filteredProps[prop.displayName][prop.displayValue] += 1;
});
});
// Now ready to show!
// The PropertyPanel has the .addProperty that receives the name, value
// and category, that simple! So just iterate through the list and add them
filteredProps.forEach((prop) => {
if (filteredProps[prop] === undefined) return;
Object.keys(filteredProps[prop]).forEach((val) => {
this._panel.addProperty(val, filteredProps[prop][val], prop);
this.dt = new DataTabe(this._panel);
this.dt.setData()
});
});
});
});
};
this._button.setToolTip('Or Levis extenssion');
this._button.addClass('testTest');
this._group.addControl(this._button);
}
}
Autodesk.Viewing.theExtensionManager.registerExtension('testTest', testTest);'''
'''
We have a tutorial with step-by-step on how to do it.
Please, refer to Dashboard tutorial, specifically in Data Grid section.
In this case, the tutorial uses an external library (Tabulator) to show the data.

check store for object before calling api

You know how they say you don't need state management until you know you need it. Well turns out my project needs it. So I need some help wit best practice as I am adding ngxs to an existing angular project.
I have an action called getServiceDetail and my statemodel has a list of objects called DriverListsStopInfoViewModel. each of these objects have a unique ID. The html template of the consuming component uses a selector for the property currentStopDetail, which is a state property that gets set in my action.
GOAL:
in my action I want to check the list of objects in my store to see if an object with the same id exists and return that object, and if it does not exist, call and api to get it.
EXAMPLE:
The following code works, but I would like to hear if this is the right way to do it. do I even need to return the object from the action function if its found, or can I just use patch state to assign it to the currentStopDetail
export interface SignServiceStateModel {
searchResults: ServiceSearchModel[];
driverStopsDetails: DriverListsStopInfoViewModel[];
driverStopsList: DriverListsStopsViewModel[];
driverStopsMarkers: DriverStopsMarkerViewModel[];
currentStopDetail: DriverListsStopInfoViewModel;
}
const SIGNSERVICE_STATE_TOKEN = new StateToken<SignServiceStateModel>(
'signservice'
);
#State<SignServiceStateModel>({
name: SIGNSERVICE_STATE_TOKEN,
defaults: {
searchResults: [],
driverStopsDetails: [],
driverStopsList: [],
driverStopsMarkers: [],
currentStopDetail: null
},
})
#Injectable()
export class SignServiceState {
constructor(private driverListsService: DriverListsService) {}
#Action(DriverList.GetServiceDetail)
getServiceDetail(
ctx: StateContext<SignServiceStateModel>,
action: DriverList.GetServiceDetail
) {
if (action.serviceId === undefined || action.serviceId <= 0) {
return;
}
// check if record already in list and return
const currentState = ctx.getState();
const existingStopDetail = currentState.driverStopsDetails.find(s => s.SignServiceId === action.serviceId);
if (existingStopDetail !== undefined) {
const currentStopDetail = existingStopDetail;
ctx.patchState({ currentStopDetail });
return currentStopDetail;
}
// else get new record, add it to list and return
return this.driverListsService.getDriverListsInfo(action.serviceId).pipe(
tap((currentStopDetail) => {
ctx.patchState({ currentStopDetail });
ctx.setState(
patch({
driverStopsDetails: append([currentStopDetail])
})
);
})
);
}
#Selector()
static currentStopDetail(state: SignServiceStateModel) {
return state.currentStopDetail;
}
}
I only included the relevant code from my state class
QUESTION:
is this the best way to check the store for an item and call api if it does not exist?
Thanks in advance
Short answer is yes, what you have done here is a typical way of handling this scenario (in my experience). There's a couple of improvements you could make:
do I even need to return the object from the action function if its found, or can I just use patch state to assign it to the currentStopDetail
No, you don't return anything from these action handlers, other than possibly an Observable that NGXS will handle (so in your case if there is no matching item found, you return the Observable that fetchs it from the API and patches the state).
Also when you do make the API call, you should only need a single update to the state:
return this.driverListsService.getDriverListsInfo(action.serviceId).pipe(
tap((result) => {
ctx.setState(
patch({
currentStopDetails: result
driverStopsDetails: append([result]),
})
);
})
);

How to properly update nested data in redux

This is my current object which i need to update:
[
{ id: q1,
answers:[
{ id: a1,
answered: false
},
...
]
},
...
]
I can't figure out how to update this object and set for example answered = true.
Is there any better way saving this kind of object? I tried to use the update addon from React but can't get it to work properly.
You can update the answers list this way, in your reducer:
function update(state, action) {
// assuming you are passing an id of the item to be updated via action.itemId
let obj = state.whatever_list.filter(item => item.id === action.itemId)[0]
//assuming you are passing an id of the answer to be updated via action.answerId
//also, assuming action.payload contains {answered: true}
let answers = obj.answers.map(answer => answer.id === action.answerId ?
Object.assign({}, answer, action.payload) : answer)
obj = Object.assign({}, obj, {answers: answers})
return {
whatever_list: state.whatever_list.map(item => item.id == action.itemId? Object.assign({}, item, obj) : item)
}
}
Here is what your action might look like:
function updateAnswer(itemId, answerId, payload) {
return {
type: UPDATE_ANSWER,
itemId: itemId,
answerId: answerId,
payload: payload
}
}
In your react component class, assuming there is an event handler for monitoring whether if a question is answered:
export default class Whatever extends React.Component {
...
// assuming your props contains itemId and answerId
handleAnswered = (e) => {
this.props.dispatch(updateAnswer(this.props.itemId, this.props.answerId, {answered: true}))
}
...
}
So basically what happens is this:
Your event handler calls the action and pass the updated data to it
When your action is called, it returns the updated data along with a type parameter
When your reducer sees the type parameter, the corresponding handler will be triggered (the first piece of the code above)
The reducer will pull out the existing data from the list, replace the old data with the new one, and then return a list containing the new data
You can create a sub-reducer for the answers key. Look at this example:
https://github.com/rackt/redux/blob/master/examples/async/reducers/index.js
You could use dot-prop-immutable and an update would be as simple as:
return dotProp.set(state, 'quiz.0.answers.0.answered', true);

linqToSql related table not delay loading properly. Not populating at all

I have a couple of tables with similar relationship structure to the standard Order, OrderLine tables.
When creating a data context, it gives the Order class an OrderLines property that should be populated with OrderLine objects for that particular Order object.
Sure, by default it will delay load the stuff in the OrderLine property but that should be fairly transparent right?
Ok, here is the problem I have: I'm getting an empty list when I go MyOrder.OrderLines but when I go myDataContext.OrderLines.Where(line => line.OrderId == 1) I get the right list.
public void B()
{
var dbContext = new Adis.CA.Repository.Database.CaDataContext(
"<connectionString>");
dbContext.Connection.Open();
dbContext.Transaction = dbContext.Connection.BeginTransaction();
try
{
//!!!Edit: Imortant to note that the order with orderID=1 already exists
//!!!in the database
//just add some new order lines to make sure there are some
var NewOrderLines = new List<OrderLines>()
{
new OrderLine() { OrderID=1, LineID=300 },
new OrderLine() { OrderID=1, LineID=301 },
new OrderLine() { OrderID=1, LineID=302 },
new OrderLine() { OrderID=1, LineID=303 }
};
dbContext.OrderLines.InsertAllOnSubmit(NewOrderLines);
dbContext.SubmitChanges();
//this will give me the 4 rows I just inserted
var orderLinesDirect = dbContext.OrderLines
.Where(orderLine => orderLine.OrderID == 1);
var order = dbContext.Orders.Where(order => order.OrderID == 1);
//this will be an empty list
var orderLinesThroughOrder = order.OrderLines;
}
catch (System.Data.SqlClient.SqlException e)
{
dbContext.Transaction.Rollback();
throw;
}
finally
{
dbContext.Transaction.Rollback();
dbContext.Dispose();
dbContext = null;
}
}
So as far as I can see, I'm not doing anything particularly strange but I would think that orderLinesDirect and orderLinesThroughOrder would give me the same result set.
Can anyone tell me why it doesn't?
You're just adding OrderLines; not any actual Orders. So the Where on dbContext.Orders returns an empty list.
How you can still find the property OrderLines on order I don't understand, so I may be goofing up here.
[Edit]
Could you update the example to show actual types, especially of the order variable? Imo, it shoud be an IQueryable<Order>, but it's strange that you can .OrderLines into that. Try adding a First() or FirstOrDefault() after the Where.

Resources