Skip to content

Commit

Permalink
[WIP] Decouple workspace saves from navigational changes
Browse files Browse the repository at this point in the history
Connect to #167

This is a work-in-progress commit, as I have now unassigned from #167. Feel free to use this or ignore it or dump it.
  • Loading branch information
mjgiarlo committed Jul 12, 2021
1 parent e676a31 commit 98aafad
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 47 deletions.
18 changes: 16 additions & 2 deletions app/controllers/workspaces_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,25 @@ def update
respond_to do |format|
if @workspace.update(workspace_params)
format.html { redirect_to @workspace, notice: update_notice }
format.json { render :show, status: :ok, location: @workspace }
format.js { render json: {}, status: :ok }
format.json do
# render :show, status: :ok, location: @workspace
# Handle an XHR request to save a workspace by returning the flash messages pre-rendered
flash.now[:notice] = update_notice
render json: { flash: render_to_string(partial: '/flash_messages', formats: [:html]), persistedState: @workspace.state }
end
format.js do
# Handle an XHR request to save a workspace by returning the flash messages pre-rendered
flash.now[:notice] = update_notice
render json: { flash: render_to_string(partial: '/flash_messages'), state: @workspace.state }
end
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @workspace.errors, status: :unprocessable_entity }
format.js do
# Handle an XHR request to save a workspace by returning the flash messages pre-rendered
flash.now[:alert] = "Your workspace could not be saved: #{@workspace.errors}"
render partial: '/flash_messages'
end
end
end
end
Expand Down
57 changes: 33 additions & 24 deletions app/javascript/components/Viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ class Viewer extends React.Component {
constructor() {
super();
this.state = {
currentState: {},
viewerState: {},
};
}

/** */
componentDidMount() {
const {
annototEndpointUrl, config, enabledPlugins, state, updateStateSelector, projectResourcesUrl,
annototEndpointUrl, config, enabledPlugins, initialState,
viewerStateSelector, projectResourcesUrl,
} = this.props;

delete config.export;
Expand All @@ -48,26 +49,26 @@ class Viewer extends React.Component {
],
);

if (state) {
if (initialState) {
instance.store.dispatch(
importMiradorState(
{
...state,
...initialState,
config: instance.store.getState().config,
},
),
);
}

if (projectResourcesUrl) instance.store.dispatch(addResource(projectResourcesUrl));
if (updateStateSelector) {
if (viewerStateSelector) {
instance.store.subscribe(() => {
const currentState = instance.store.getState();
const exportableState = getExportableState(currentState);
this.setState({ currentState: exportableState });
const inputElement = document.querySelector(updateStateSelector);
const viewerState = instance.store.getState();
const exportableState = getExportableState(viewerState);
this.setState({ viewerState: exportableState });
const inputElement = document.querySelector(viewerStateSelector);
const __mise_cache__ = { // eslint-disable-line camelcase
manifests: mapValues(currentState.manifests, m => this.getManifestCache(currentState, m)),
manifests: mapValues(viewerState.manifests, m => this.getManifestCache(viewerState, m)),
};
if (inputElement) {
inputElement.value = JSON.stringify(
Expand Down Expand Up @@ -112,22 +113,30 @@ class Viewer extends React.Component {
}

checkUnsavedChanges = (event) => {
const { currentState } = this.state;
const { state: initialState, saveInProgressSelector } = this.props;
// Skip checking for unsaved changes because a save is in progress
if (document.querySelector(saveInProgressSelector).value === 'true') return true;
const { viewerState } = this.state;
const { persistedStateSelector } = this.props;
// Get current persisted value of DOM element instead of using what is
// stuffed in props since the former may have been updated by a
// user-initiated save operation
const persistedState = document.querySelector(persistedStateSelector).value;

const expectedDiffPaths = ['__mise_cache__', 'config.annotation', 'config.export', 'workspace.id'];
// Diff the two Mirador states
const difference = diff(initialState, currentState);
const difference = diff(persistedState, viewerState);
// Remove known false positives from diff object
const filtered = omit(difference, expectedDiffPaths);
// Remove empty top-level objects created by prior call to omit
const filtered = omit(difference, ['__mise_cache__', 'config.annotation', 'config.export', 'workspace.id']);
// Remove empty top-level objects created by removing false positives
const changes = omitBy(filtered, isEmpty);

if (!isEmpty(changes)) {
console.dir(changes);
event.preventDefault();
event.returnValue = 'If you navigate away from the workspace now, changes you have made will be lost. Are you sure you want to navigate away?'; // eslint-disable-line no-param-reassign
return event.returnValue;
} else {
console.log('client state:');
console.dir(viewerState);
console.log('server state:');
console.dir(persistedState);
}
return true;
}
Expand All @@ -143,20 +152,20 @@ Viewer.propTypes = {
annototEndpointUrl: PropTypes.string,
config: PropTypes.object, // eslint-disable-line react/forbid-prop-types
enabledPlugins: PropTypes.array, // eslint-disable-line react/forbid-prop-types
initialState: PropTypes.object, // eslint-disable-line react/forbid-prop-types
persistedStateSelector: PropTypes.string,
projectResourcesUrl: PropTypes.string,
saveInProgressSelector: PropTypes.string,
state: PropTypes.object, // eslint-disable-line react/forbid-prop-types
updateStateSelector: PropTypes.string,
viewerStateSelector: PropTypes.string,
};

Viewer.defaultProps = {
annototEndpointUrl: null,
config: {},
enabledPlugins: [],
initialState: null,
persistedStateSelector: null,
projectResourcesUrl: null,
saveInProgressSelector: null,
state: null,
updateStateSelector: null,
viewerStateSelector: null,
};

export default Viewer;
9 changes: 0 additions & 9 deletions app/javascript/controllers/save_workspace_controller.js

This file was deleted.

11 changes: 11 additions & 0 deletions app/javascript/controllers/workspace_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Controller } from 'stimulus';

export default class extends Controller {
static targets = ['flash', 'persistedState'];

updateForm(event) {
const { flash, persistedState } = event.detail[0];
this.persistedStateTarget.value = JSON.stringify(persistedState);
this.flashTarget.innerHTML = flash;
}
}
File renamed without changes.
6 changes: 4 additions & 2 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>

<body data-controller='show-modal'>
<body data-controller='show-modal workspace'>
<%= render 'layouts/navbar' %>
<div class="main-content <%= 'with-sidebar' if content_for?(:sidebar) %>">
<% if content_for?(:sidebar) %>
Expand All @@ -20,7 +20,9 @@
<% end %>

<main class='col py-4 pe-4'>
<%= render 'layouts/flash_messages' %>
<div class="flash_container" data-workspace-target="flash">
<%= render '/flash_messages' %>
</div>

<%= content_for(:top_level_nav) if content_for?(:top_level_nav) %>

Expand Down
25 changes: 15 additions & 10 deletions app/views/workspaces/viewer.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@
<% if can? :update, @workspace %>
<%= react_component("EditInPlace", { field: 'title', value: @workspace.title, csrfToken: form_authenticity_token, url: url_for(@workspace) }, tag: 'h1') %>

<%= form_with(model: @workspace, class: 'me-2', data: { controller: 'save-workspace' }) do |form| %>
<%= form.hidden_field :state %>
<%= form.hidden_field :save_in_progress, value: false, data: { save_workspace_target: 'field' } %>
<%= form.submit 'Save workspace', class: 'btn btn-primary', data: { action: 'save-workspace#markSaveInProgress' } %>
<%=
form_with(model: @workspace, class: 'me-2', local: false, data: {
action: 'ajax:success->workspace#updateForm ajax:error->workspace#updateForm',
type: :json
}) do |form|
%>
<%= form.hidden_field :viewer_state, data: { workspace_target: 'viewerState' } %>
<%= form.hidden_field :persisted_state, data: { workspace_target: 'persistedState' } %>
<%= form.submit 'Save workspace', class: 'btn btn-primary' %>
<% end %>
<% else %>
<h1><%= @workspace.title %></h1>
Expand All @@ -21,20 +26,20 @@
<div class="<%= @embed ? 'embedded-mirador-container' : 'mirador-container mx-2' %>">
<% viewer_props = if can?(:update, @workspace) && !@embed
{
annototEndpointUrl: project_annotations_url(@workspace.project),
config: { id: "m-#{SecureRandom.hex}" }.deep_merge(@workspace.state&.dig('config') || {}),
enabledPlugins: ['annotations'],
state: @workspace.state,
updateStateSelector: '#workspace_state',
saveInProgressSelector: '#workspace_save_in_progress',
initialState: @workspace.state,
persistedStateSelector: '#workspace_persisted_state',
projectResourcesUrl: iiif_project_resources_url(@workspace.project),
annototEndpointUrl: project_annotations_url(@workspace.project),
viewerStateSelector: '#workspace_viewer_state',
}
else
{
annototEndpointUrl: project_annotations_url(@workspace.project),
config: { id: "m-#{SecureRandom.hex}" }.deep_merge(@workspace.embedded_workspace_config || {}),
enabledPlugins: ['annotations'],
state: @workspace.state,
annototEndpointUrl: project_annotations_url(@workspace.project),
initialState: @workspace.state,
}
end
%>
Expand Down

0 comments on commit 98aafad

Please sign in to comment.