kopia lustrzana https://github.com/wagtail/wagtail
Update ping and release URLs on every editing session ping
In case the original session has been cleaned uppull/12185/head
rodzic
1139f2a36e
commit
edb3a1ab80
|
@ -1,6 +1,8 @@
|
|||
import { Application } from '@hotwired/stimulus';
|
||||
import { SessionController } from './SessionController';
|
||||
import { DialogController } from './DialogController';
|
||||
import { SwapController } from './SwapController';
|
||||
import { ActionController } from './ActionController';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
|
@ -10,6 +12,8 @@ describe('SessionController', () => {
|
|||
beforeAll(() => {
|
||||
application = Application.start();
|
||||
application.register('w-session', SessionController);
|
||||
application.register('w-swap', SwapController);
|
||||
application.register('w-action', ActionController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -389,4 +393,189 @@ describe('SessionController', () => {
|
|||
expect(new FormData(form).get('is_editing')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updating the session state based on JSON data from an event', () => {
|
||||
describe('with complete controller configuration', () => {
|
||||
afterEach(() => {
|
||||
fetch.mockRestore();
|
||||
});
|
||||
|
||||
let element;
|
||||
|
||||
const setup = async () => {
|
||||
document.body.innerHTML = /* html */ `
|
||||
<form
|
||||
method="post"
|
||||
data-controller="w-swap w-action w-session"
|
||||
data-w-swap-target-value="#w-editing-sessions"
|
||||
data-w-swap-json-path-value="html"
|
||||
data-w-swap-src-value="http://localhost/sessions/1/"
|
||||
data-w-action-url-value="http://localhost/sessions/1/release/"
|
||||
data-action="w-session:ping->w-swap#submit w-swap:json->w-session#updateSessionData"
|
||||
>
|
||||
<div id="w-editing-sessions"></div>
|
||||
</form>
|
||||
`;
|
||||
element = document.querySelector('form');
|
||||
await Promise.resolve();
|
||||
};
|
||||
|
||||
it('should update the SwapController and ActionController URL values', async () => {
|
||||
fetch.mockResponseSuccessJSON(
|
||||
JSON.stringify({
|
||||
html: '',
|
||||
ping_url: 'http://localhost/sessions/2/',
|
||||
release_url: 'http://localhost/sessions/2/release/',
|
||||
}),
|
||||
);
|
||||
await setup();
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost/sessions/1/',
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
// Simulate request finishing
|
||||
await Promise.resolve();
|
||||
|
||||
// Simulate JSON parsing
|
||||
await Promise.resolve();
|
||||
|
||||
expect(element.dataset.wSwapSrcValue).toEqual(
|
||||
'http://localhost/sessions/2/',
|
||||
);
|
||||
expect(element.dataset.wActionUrlValue).toEqual(
|
||||
'http://localhost/sessions/2/release/',
|
||||
);
|
||||
|
||||
await Promise.resolve();
|
||||
expect(document.getElementById('w-editing-sessions').innerHTML).toEqual(
|
||||
'',
|
||||
);
|
||||
|
||||
// Simulate ping after 10s
|
||||
fetch.mockResponseSuccessJSON(
|
||||
JSON.stringify({
|
||||
html: '<ul><li>Session 7</li></ul>',
|
||||
ping_url: 'http://localhost/sessions/999/',
|
||||
release_url: 'http://localhost/sessions/release/999/',
|
||||
}),
|
||||
);
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost/sessions/2/',
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
// Simulate request finishing
|
||||
await Promise.resolve();
|
||||
|
||||
// Simulate JSON parsing
|
||||
await Promise.resolve();
|
||||
|
||||
expect(element.dataset.wSwapSrcValue).toEqual(
|
||||
'http://localhost/sessions/999/',
|
||||
);
|
||||
expect(element.dataset.wActionUrlValue).toEqual(
|
||||
'http://localhost/sessions/release/999/',
|
||||
);
|
||||
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
expect(document.getElementById('w-editing-sessions').innerHTML).toEqual(
|
||||
'<ul><li>Session 7</li></ul>',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle unexpected data gracefully', async () => {
|
||||
fetch.mockResponseSuccessJSON(
|
||||
JSON.stringify({
|
||||
html: '<ul><li>Session 1</li></ul>',
|
||||
}),
|
||||
);
|
||||
await setup();
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost/sessions/1/',
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
// Simulate request finishing
|
||||
await Promise.resolve();
|
||||
|
||||
// Simulate JSON parsing
|
||||
await Promise.resolve();
|
||||
|
||||
// Should not update the URL values
|
||||
expect(element.dataset.wSwapSrcValue).toEqual(
|
||||
'http://localhost/sessions/1/',
|
||||
);
|
||||
expect(element.dataset.wActionUrlValue).toEqual(
|
||||
'http://localhost/sessions/1/release/',
|
||||
);
|
||||
|
||||
// Should still update the HTML
|
||||
await Promise.resolve();
|
||||
expect(document.getElementById('w-editing-sessions').innerHTML).toEqual(
|
||||
'<ul><li>Session 1</li></ul>',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with improper configuration', () => {
|
||||
let element;
|
||||
beforeEach(async () => {
|
||||
document.body.innerHTML = /* html */ `
|
||||
<div
|
||||
data-controller="w-session"
|
||||
data-action="w-swap:json->w-session#updateSessionData"
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
element = document.querySelector('[data-controller="w-session"]');
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should handle the data gracefully even if SwapController and ActionController are not present', async () => {
|
||||
const mock = jest.spyOn(
|
||||
SessionController.prototype,
|
||||
'updateSessionData',
|
||||
);
|
||||
element.dispatchEvent(
|
||||
new CustomEvent('w-swap:json', {
|
||||
detail: {
|
||||
data: {
|
||||
html: '<ul><li>Session 2</li></ul>',
|
||||
ping_url: 'http://localhost/sessions/999/',
|
||||
release_url: 'http://localhost/sessions/release/999/',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mock).toHaveBeenCalledTimes(1);
|
||||
expect(element.dataset.wSwapSrcValue).toBeUndefined();
|
||||
expect(element.dataset.wActionUrlValue).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle the event gracefully even if it does not contain data', async () => {
|
||||
const mock = jest.spyOn(
|
||||
SessionController.prototype,
|
||||
'updateSessionData',
|
||||
);
|
||||
element.dispatchEvent(
|
||||
new CustomEvent('w-swap:json', { detail: { foo: 'bar' } }),
|
||||
);
|
||||
|
||||
expect(mock).toHaveBeenCalledTimes(1);
|
||||
expect(element.dataset.wSwapSrcValue).toBeUndefined();
|
||||
expect(element.dataset.wActionUrlValue).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,21 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import { DialogController } from './DialogController';
|
||||
import { SwapController } from './SwapController';
|
||||
import { ActionController } from './ActionController';
|
||||
|
||||
interface PingResponse {
|
||||
session_id: string;
|
||||
ping_url: string;
|
||||
release_url: string;
|
||||
other_sessions: {
|
||||
session_id: string | null;
|
||||
user: string;
|
||||
last_seen_at: string;
|
||||
is_editing: boolean;
|
||||
revision_id: number | null;
|
||||
}[];
|
||||
html: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage an editing session by indicating the presence of the user and handling
|
||||
|
@ -203,6 +219,43 @@ export class SessionController extends Controller<HTMLElement> {
|
|||
this.unsavedChangesTarget.checked = type !== 'clear';
|
||||
}
|
||||
|
||||
get swapController() {
|
||||
return this.application.getControllerForElementAndIdentifier(
|
||||
this.element,
|
||||
'w-swap',
|
||||
) as SwapController | null;
|
||||
}
|
||||
|
||||
get actionController() {
|
||||
return this.application.getControllerForElementAndIdentifier(
|
||||
this.element,
|
||||
'w-action',
|
||||
) as ActionController | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the session state with the latest data from the server.
|
||||
* @param event an event that contains JSON data in the `detail` property. Normally a `w-swap:json` event.
|
||||
*/
|
||||
updateSessionData(event: CustomEvent) {
|
||||
const { detail } = event;
|
||||
if (!detail || !detail.data) return;
|
||||
const data: PingResponse = detail.data;
|
||||
|
||||
// Update the ping and release URLs in case the session ID has changed
|
||||
// e.g. when the user has been inactive for too long and resumed their session.
|
||||
// Modify the values via the controllers directly instead of setting the data
|
||||
// attributes so we get type checking.
|
||||
const swapController = this.swapController;
|
||||
if (swapController && data.ping_url) {
|
||||
swapController.srcValue = data.ping_url;
|
||||
}
|
||||
const actionController = this.actionController;
|
||||
if (actionController && data.release_url) {
|
||||
actionController.urlValue = data.release_url;
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
if (this.interval) {
|
||||
window.clearInterval(this.interval);
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
data-w-action-continue-value="true"
|
||||
data-w-action-url-value="{{ release_url }}"
|
||||
data-w-session-w-dialog-outlet="[data-edit-form] [data-controller='w-dialog']#w-overwrite-changes-dialog"
|
||||
data-action="w-session:ping->w-swap#submit visibilitychange@document->w-session#dispatchVisibilityState w-session:visible->w-session#ping w-session:visible->w-session#addInterval w-session:hidden->w-session#clearInterval w-session:hidden->w-action#sendBeacon w-unsaved:add@document->w-session#setUnsavedChanges w-unsaved:clear@document->w-session#setUnsavedChanges"
|
||||
data-action="w-session:ping->w-swap#submit visibilitychange@document->w-session#dispatchVisibilityState w-session:visible->w-session#ping w-session:visible->w-session#addInterval w-session:hidden->w-session#clearInterval w-session:hidden->w-action#sendBeacon w-unsaved:add@document->w-session#setUnsavedChanges w-unsaved:clear@document->w-session#setUnsavedChanges w-swap:json->w-session#updateSessionData"
|
||||
>
|
||||
<input type="checkbox" name="is_editing" value="1" data-w-session-target="unsavedChanges" hidden />
|
||||
{% if revision_id %}
|
||||
|
|
|
@ -4,6 +4,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.http import Http404, JsonResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
|
@ -162,6 +163,13 @@ def ping(request, app_label, model_name, object_id, session_id):
|
|||
return JsonResponse(
|
||||
{
|
||||
"session_id": session.id,
|
||||
"ping_url": reverse(
|
||||
"wagtailadmin_editing_sessions:ping",
|
||||
args=(app_label, model_name, object_id, session.id),
|
||||
),
|
||||
"release_url": reverse(
|
||||
"wagtailadmin_editing_sessions:release", args=(session.id,)
|
||||
),
|
||||
"other_sessions": [
|
||||
{
|
||||
"session_id": other_session["session_id"],
|
||||
|
|
Ładowanie…
Reference in New Issue