Stop operation in change() - ckeditor5

Is there a way to stop an remove operation in model.document.on('change') ?
I listen to change with this code:
model.document.on('change',(eventInfo,batch) => {
// My code here.
}
And it works fine, in so far as I do get and can inspect all changes. But there does not appear to be any way to reject the change.
I tried to call eventInfo.stop() and reset() on the differ. Both of these methods does stop the change, but always later results in a model-nodelist-offset-out-of-bounds:
exception if I try to stop a remove operation.
What I am trying to do is to change how text delete works, so when the user delete text, instead of really deleting the text from the editor, I create a marker which marks which text have been "deleted" by the user. (For optional change control).

Instead of stopping the change, maybe you could save the data value after a change and "reset" to the previous value when a delete happens:
var oldData = '';
var editor = ClassicEditor
.create(document.querySelector('#editor'))
.then(editor => {
editor.model.document.on('change',(eventInfo, batch) => {
if(oldData.length > editor.getData().length) {
// or use eventInfo.source.differ.getChanges() and check for type: "remove"
editor.setData(oldData);
}
oldData = editor.getData();
});
})
.catch( error => {
console.error( error );
});
Note instead of checking the data length, you could loop through the changes happened using eventInfo.source.differ.getChanges() and checking if there were changes of type "remove".

An operation that was already applied (and the change event is fired after operations are applied) cannot be silently removed. They need to be either reverted (by applying new operations which will revert the state of the model to the previous one) or prevented before they actually get applied.
Reverting operations
The most obvious but extremely poorly performing solution is to use setData(). That will, however, reset the selection and may cause other issues.
A far more optimal solution is to apply reversed operations for each applied operation that you want to revert. Think like in git – you cannot remove a commit (ok, you can, but you'd have to do a force push, so you don't). Instead, you apply a reversed commit.
CKEditor 5's operations allow getting their reversed conterparts. You can then apply those. However, you need to make sure that the state of the model is correct after you do that – this is tricky because when working on such a low level, you lose the normalization logic which is implemented in the model writer.
Another disadvantage of this solution is that you will end up with empty undo steps. Undo steps are defined by batches. If you'll add to a batch operations which revert those which are already in it, that batch will be an empty undo step. Currently, I don't know of a mechanism which would allow removing it from the history.
Therefore, while reverting operations is doable, it's tricky and may not work flawlessly at the moment.
Preventing applying operations
This is a solution that I'd recommend. Instead of fixing an already changed model, make sure that it doesn't change at all.
CKEditor 5 features are implemented in the form of commands. Commands have their isEnabled state. If a command is disabled, it cannot be executed.
In CKEditor 5 even features such as support for typing are implemented in the form of commands (input, delete and forwardDelete). Hence, preventing typing can be achieved by disabling these commands.

Related

SubmitForm then Patch results in "The data returned by the service was invalid"

I'm building a PowerApps app on Azure SQL
The requirement
I have a form which has "Save" and "Confirm" buttons.
Both buttons should save the form data. The Commit button should also set database column "Confirm" to 1
I've read at length about how I can programatically override the update value of a hidden control for this. But I'm not satisfied with the level of complexity (maintenance) required to get this working, i.e.
Populate a variable with the current db value
In the button code set the variable value
In the form field, set the update property to the variable
What I'm Trying
So I'm trying a different approach: SubmitForm then Patch. Even though this requires an extra database call, I'd like to understand if this will work. This is the code for OnSelect in the commit button:
// Save the record
SubmitForm(frmEdit);
// Update confirmed to 1
Patch('[dbo].[Comments]',cRecord,{Confirmed:1});
Some Complexities
Note that my record is a variable, cRecord. In short I want this app to be able to upsert based on URL parameters.
This is my App.OnStart which captures URL values, inserts a record if required. Regardless, the result of this event is that cRecord is set to the record to be edited.
// Cache employees and store lookups (as they are in a different db)
Concurrent(Collect(cEmployees, Filter('[dbo].[SalesPerson]', Status = "A")),Collect(cStores, '[dbo].[Store]'));
// Check for parameters in the URL. If found, set to Edit/Add mode
Set(bURLRecord,If((!IsBlank(Param("PersonId")) && !IsBlank(Param("Date"))),true,false));
// If URL Parameters were passed, create the record if it doesn't exist
If(bURLRecord,
Set(pPersonId,Value(Param("PersonId")));
Set(pDate,DateValue(Param("Date")));
// Try and find the record
Set(cRecord,LookUp('[dbo].[Comments]',SalesPersonId=pPersonId && TransactionDate = pDate));
If(IsBlank(cRecord),
// If the record doesn't exist, create it with Patch and capture the resulting record
Set(cRecord,Patch('[dbo].[Comments]',Defaults('[dbo].[Comments]'),{SalesPersonId:pPersonId,TransactionDate:pDate}))
);
// Navigate to the data entry screen. This screen uses cRecord as its item
Navigate(scrEdit);
)
frmEdit.Item is set to cRecord. As an aside I also have a gallery that sets this variable value when clicked so we can also navigate here from a gallery.
The navigating using new and existing URL parameters works. Navigating from the gallery works.
The problem
When I press the Commit button against a record which has Confirmed=0 I get this popup error:
The data returned by the service is invalid
When I run this code against a record which already has Confirmed=1 I don't get an error
If I run the PowerApps monitor it doesn't show any errors but it does show some counts being run after the update. I can paste it here if required.
I also tried wrapping the Path in a Set in case it's result was confusing the button event but it didn't make a difference.
What I want
So can anyone offer me any of the following info:
How can I get more info about "The data returned by the service is invalid"?
How can I get this to run without erroring?
Is there a simpler way to do the initial upsert? I was hoping a function called Patch could upsert in one call but it seems it can't
With regards to the setting field beforehand approach, I'm happy to try this again but I had some issues implementing it - understanding which control where to edit.
Any assistance appreciated.
Edit
As per recommendations in the answer, I moved the patch code into OnSuccess
If(Confirmed=1,Patch('[dbo].[CoachingComments]',cRecord,{Confirmed:1}));
But now I get the same error there. Worse I cleared out OnSucces and just put SubmitForm(frmEdit); into the OnSelect event and it is saving data but still saying
The data returned by the service was invalid
First things first,
Refactoring has multiple steps,
I can t type all out at once...
The submitform and patch issue:
Never use the submitforn with extra conplexity
Submitform is only the trigger to send the form out,
The form handler will work with your data...
If you hsven t filled out the form correctly, u don t want to trigger your patch action...
So:
On your form, you have an OnSucces property,
Place your patch code there...
Change your cRecord in your patch statement:
YourForm.LastSubmit

Custom workflow rule to set default comment visibility

I am trying to add a workflow rule in YouTrack, which sets the visibility of newly posted comments that aren't made by a specific user.
Here's what I've tried:
rule set comment visibility to developers
when comments.added.last.author.login != "special" {
comments.last.permittedGroup = {group: Developers};
}
This works, in that all newly added comments that are not made by the special user are set as visible only to Developers. The problem is that this rule also prevents the visibility from being overridden; the visibility always reverts back to "Developers", after it is changed manually via the UI.
Obviously the rule in its current form is pretty simple and I guess (hope) there's a way to isolate the creation of a comment, rather than any update to it (which I guess is what it's currently catching).
Is there any way to only have this rule apply to newly created comments, rather than to any that have been updated?
The trick here is that YouTrack Workflow Language supports null-safety. When you add a comment, comments.added.last contains the comment, and a rule works as expected. When you edit something else, comments.added.last is null, thus comments.added.last.author.login is also null, and null != "special", indeed.
What you need is to check that there are newly created comments is this change, e.g.
when comments.added.isNotEmpty && comments.added.last.author.login != "special" {
comments.last.permittedGroup = {group: Developers};
}

Whats wrong with my code (GML)

ok so im sorry to be asking, however im trying to make it so that when i press z, a portal appears at my Spr_players coordinates, however if one of them already exists, i want it to be erased and im simply wondering what ive done wrong. Again sorry for bothering. (please note that i am a bad programmer and i appoligize if i broke any rules)
if object_exists(portal)
{
instance_destroy()
action_create_object(portal,Spr_player.x,Spr_player.y)
}
else
{
action_create_object(portal,Spr_player.x,Spr_player.y)
}
The instance_destroy() statement destroys the current self instance which is what is executing the code. You must use the with (<objectID>) {instance_destroy()} syntax to destroy another instance.
As long as there is only one instance of portal in the room this code should work:
if object_exists(portal)
{
with (portal) instance_destroy(); //you should also need a semicolon here to separate
//this statement from the next, it is good practice
//to do this after all statements as I have done.
action_create_object(portal,Spr_player.x,Spr_player.y);
}
else
{
action_create_object(portal,Spr_player.x,Spr_player.y);
}
If there are multiple instances of portal this will only destroy the first one. To destroy all you would have to use a for loop to iterate through them all. Off the top of my mind I can not remember the function to get the ids of all the instances of an object, but it looks like this is not a problem since each time one is created the existing one is destroyed and thus you will only have one a t a time.
Another way of doing this is just to move the existing portal to the new position. The only difference here is that the create event of the portal will not be executed and any alarms will not be reset.
portal.x=Spr_player.x
portal.y=Spr_player.y
Again this will only move the first portal if there are more than one.

Phalcon redirection and forwarding

Do I understand correctly that after doing $this->dispatcher->forward() or $this->response->redirect() I need to manually ensure that the rest of the code does't get executed? Like below, or am I missing something?
public function signinAction()
{
if ($this->isUserAuthenticated())
{
$this->response->redirect('/profile');
return;
}
// Stuff if he isn't authenticated…
}
After almost a year of working on a hardcore project that uses Phalcon beyond its capacity, I wanted to clarify a few things and answer my own question. To understand how to properly do redirects and forwards you need to understand a little about how the Dispatcher::dispatch method works.
Take a look at the code here, though it's all C mumbo-jumbo to most of us, its really well written and documented. In the nutshell this is what it does:
The dispatcher enters the while loop until the _finished property becomes true or it discovers a recursion.
Inside the loop, it immediately sets that property to true, so when it starts the next iteration it will automatically break.
It then gets the controller / action information, which are originally supplied by the router in the application, and does various checks. Before and after that it also completes a lot of event-related business.
Finally it calls the action method in the controller and updates the _returnedValue property with (guess what!) the returned value.
If during the action call you call Dispatcher::forward method, it will update the _finished property back to false, which will allow the while loop to continue from the step 2 of this list.
So, after you do redirect or forward, you need to ensure that you code doesn't get executed only if that is part of the expected logic. In other words you don't have to return the result of return $this->response->redirect or return $this->dispatcher->forward.
Doing the last might seem convenient, but not very correct and might lead to problems. In 99.9% cases your controller should not return anything. The exception would be when you actually know what you are doing and want to change the behaviour of the rendering process in your application by returning the response object. On top of that your IDE might complain about inconsistent return statements.
To finalise, the correct way to redirect from within the controller:
// Calling redirect only sets the 30X response status. You also should
// disable the view to prevent the unnecessary rendering.
$this->response->redirect('/profile');
$this->view->disable();
// If you are in the middle of something, you probably don't want
// the rest of the code running.
return;
And to forward:
$this->dispatcher->forward(['action' => 'profile']);
// Again, exit if you don't need the rest of the logic.
return;
You need to use it like this:
return $this->response->redirect('/profile');
or
return $this->dispatcher->forward(array(
'action' => 'profile'
))
Use send() like this
public function signinAction()
{
if ($this->isUserAuthenticated())
{
return $this->response->redirect('profile')->send();
}
}

Can I add a promise/unmaterialized record to a hasMany while I wait for it to be retrieved?

Back in the unversioned Ember Data days (e.g. "rev 12" maybe) I'm pretty sure you could do this:
var comment = App.Comment.find(42); // Already exists, but not yet loaded...
post.get('comments').addObject(comment);
Because App.Comment.find(42) would return an App.Comment object, albeit one with no fields populated except it's ID. (I don't remember the details of how you'd then save the App.Post--i.e. if you could or couldn't save it until the comment object was completely loaded…I never got that far.)
Why this was neat is that if your template rendered post.comments, a new row/div would appear immediately that could check isLoaded to display a loading indicator and show instantly that a new record was attached while waiting for the record's data to load. This is/was a selling point of Ember/Ember Data, and one I really like.
But this doesn't work now in 1.0.0-beta.2 beta.4 beta.5:
var comment = controller.get('store').find('comment', 42);
post.get('comments').addObject(comment); // Fails
Because controller.get('store').find('comment', 42) returns a promise, and if I try to add it to the hasMany it complains that I can only add App.Comment objects to the relationship.
Is it still possible to do something like this, so that my template which renders the comments immediately updates with a new record, but asynchronously populates its data?
(Please ignore that it doesn't make sense to add an already existing comment to a post--using the ubiquitous example scenario is easier than posting all my model code. Thanks!)
Okay, I came up with at least one way that does it:
var comment = controller.get('store').find('comment', 42);
var inFlightRecord = controller.get('store').getById('comment', 42);
controller.get('comments').addObject(inFlightRecord);
To be safer, maybe:
var comment = controller.get('store').find('comment', 42);
var inFlightRecord = controller.get('store').getById('comment', 42);
if(inFlightRecord){ // should be null if it isn't in the store
controller.get('comments').addObject(inFlightRecord);
} else {
// add a then block to the promise to make sure it gets added later
}
It seems that getById returns the "unloaded" object like we used to get from App.Comment.find(42), and the object still has an isLoaded property you can check to show loading status in your template.
I'm not sure if this is supposed to be supported behavior that I can rely on going forward (I suppose arguably nothing is, until 1.0 release), but it seems to work. I even checked that the object returned by getById === the object fulfilled in the promise. So this seems to be a good solution.
Anyone see a problem with this, or have a better way?