Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save as improvements #267

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
86 changes: 77 additions & 9 deletions nbclassic/static/notebook/js/notebook.js
Original file line number Diff line number Diff line change
Expand Up @@ -2866,6 +2866,7 @@ define([

Notebook.prototype.save_notebook_as = function() {
var that = this;
var current_notebook_name = $('body').attr('data-notebook-name')
var current_dir = $('body').attr('data-notebook-path').split('/').slice(0, -1).join("/");
current_dir = current_dir? current_dir + "/": "";
current_dir = decodeURIComponent(current_dir);
Expand Down Expand Up @@ -2893,12 +2894,11 @@ define([
var nb_path = d.find('input').val();
var nb_name = nb_path.split('/').slice(-1).pop();
if (!nb_name) {
$(".save-message").html(
$("<span>")
.attr("style", "color:red;")
.text($(".save-message").text())
);
return false;
// infer notebook name from current file
nb_name = current_notebook_name
let path = nb_path.split('/').slice(0, -1)
path.push(current_notebook_name)
var nb_path = path.join('/')
}
// If notebook name does not contain extension '.ipynb' add it
var ext = utils.splitext(nb_name)[1];
Expand All @@ -2922,6 +2922,8 @@ define([
that.session.rename_notebook(data.path);
that.events.trigger('notebook_renamed.Notebook', data);
that.save_notebook_success(start, data);
document.body.setAttribute('data-notebook-path', that.notebook_path)
document.body.setAttribute('data-notebook-name', that.notebook_name)
}, function(error) {
var msg = i18n.msg._(error.message || 'Unknown error saving notebook');
$(".save-message").html(
Expand All @@ -2930,8 +2932,33 @@ define([
.text(msg)
);
});
};
var getParentPath = function(path) {
return path.split('/').slice(0, -1).join('/')
};
that.contents.get(nb_path, {type: 'notebook', content: false}).then(function(data) {
function makeParentDirectory(path) {
var parent_path = getParentPath(path)
var check_parent = that.contents.get(parent_path, {type: 'directory', content: false})
var recursed = check_parent.catch(
function(err) {
return makeParentDirectory(parent_path)
}
)
var save_it = Promise.allSettled([check_parent, recursed]).then(
function() {
model = {
'type': 'directory',
'name': '',
'path': utils.url_path_split(path)[0]
}
return that.contents.save(path, model).catch();
}
)
return save_it
};

that.contents.get(nb_path, {type: 'notebook', content: false}).then(
function(data) {
var warning_body = $('<div/>').append(
$("<p/>").text(i18n.msg._('Notebook with that name exists.')));
dialog.modal({
Expand All @@ -2947,7 +2974,44 @@ define([
}
});
}, function(err) {
return save_thunk();
var nb_path_parent = getParentPath(nb_path)
that.contents.get(nb_path_parent, {type: 'directory', content: false}).then(
function (data) {
return save_thunk();
},
function(err) {
var warning_body = $('<div/>').append(
$("<p/>").text(i18n.msg._('Create missing directory?'))
);
dialog.modal({
title: 'Directory does not exist',
body: warning_body,
buttons: {
Cancel: {},
Create: {
class: 'btn-warning',
click: function() {
return makeParentDirectory(nb_path_parent).then(
function(data) {
return save_thunk()
}
).catch(
(error) => {
var msg = i18n.msg._(error.message || 'Unknown error creating directory.');
$(".save-message").html(
$("<span>")
.attr("style", "color:red;")
.text(msg)
);
}

)
}
}
}
})
}
);
});
return false;
}
Expand All @@ -2961,7 +3025,7 @@ define([
}
});
d.find('input[type="text"]').val(current_dir).focus();
}
}
});
};

Expand Down Expand Up @@ -3104,6 +3168,10 @@ define([
that._last_modified = json.last_modified;
that.session.rename_notebook(json.path);
that.events.trigger('notebook_renamed.Notebook', json);

// update document attributes after rename
document.body.setAttribute('data-notebook-path', that.notebook_path)
document.body.setAttribute('data-notebook-name', that.notebook_name)
}
);
};
Expand Down
55 changes: 34 additions & 21 deletions nbclassic/tests/end_to_end/test_save_as_notebook.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Test save-as functionality"""


from functools import partial
import traceback

from .utils import EDITOR_PAGE, EndToEndTimeout
Expand Down Expand Up @@ -50,21 +50,17 @@ def test_save_as_nb(notebook_frontend):
name_input_element = notebook_frontend.locate('.modal-body .form-control', page=EDITOR_PAGE)
name_input_element.focus()
name_input_element.click()
notebook_name = 'new_notebook.ipynb'

print('[Test] Begin attempts to fill the save dialog input and save the notebook')
fill_attempts = 0

def attempt_form_fill_and_save():
def attempt_form_fill_and_save(notebook_path):
# Application behavior here is HIGHLY variable, we use this for repeated attempts
# ....................
# This may be a retry, check if the application state reflects a successful save operation
nonlocal fill_attempts
if fill_attempts and get_notebook_name(notebook_frontend) == "new_notebook.ipynb":
if fill_attempts and get_notebook_name(notebook_frontend) == notebook_path.split("/")[-1]:
print('[Test] Success from previous save attempt!')
return True
fill_attempts += 1
print(f'[Test] Attempt form fill and save #{fill_attempts}')
print(f'[Test] Attempt form fill with directory and save #{fill_attempts}')

# Make sure the save prompt is visible
if not name_input_element.is_visible():
Expand All @@ -73,18 +69,18 @@ def attempt_form_fill_and_save():

# Set the notebook name field in the save dialog
print('[Test] Fill the input field')
name_input_element.evaluate(f'(elem) => {{ elem.value = "new_notebook.ipynb"; return elem.value; }}')
name_input_element.evaluate(f'(elem) => {{ elem.value = "{notebook_path}"; return elem.value; }}')
notebook_frontend.wait_for_condition(
lambda: name_input_element.evaluate(
f'(elem) => {{ elem.value = "new_notebook.ipynb"; return elem.value; }}') == 'new_notebook.ipynb',
f'(elem) => {{ elem.value = "{notebook_path}"; return elem.value; }}') == notebook_path,
timeout=120,
period=.25
)
# Show the input field value
print('[Test] Name input field contents:')
field_value = name_input_element.evaluate(f'(elem) => {{ return elem.value; }}')
print('[Test] ' + field_value)
if field_value != 'new_notebook.ipynb':
if field_value != notebook_path:
return False

print('[Test] Locate and click the save button')
Expand All @@ -93,6 +89,14 @@ def attempt_form_fill_and_save():
save_element.focus()
save_element.click()

# If the notebook path contains a directory, click the create button
if "/" in notebook_path:
print('[Test] Locate and click the create button')
create_element = dialog_element.locate('text=Create')
create_element.wait_for('visible')
create_element.focus()
create_element.click()

# Application lag may cause the save dialog to linger,
# if it's visible wait for it to disappear before proceeding
if save_element.is_visible():
Expand All @@ -105,18 +109,27 @@ def attempt_form_fill_and_save():

# Check if the save operation succeeded (by checking notebook name change)
notebook_frontend.wait_for_condition(
lambda: get_notebook_name(notebook_frontend) == "new_notebook.ipynb", timeout=120, period=5
lambda: get_notebook_name(notebook_frontend) == notebook_path.split('/')[-1], timeout=120, period=5
)
print(f'[Test] Notebook name: {get_notebook_name(notebook_frontend)}')
print('[Test] Notebook name was changed!')
return True

# Retry until timeout (wait_for_condition retries upon func exception)
notebook_frontend.wait_for_condition(attempt_form_fill_and_save, timeout=900, period=1)

print('[Test] Check notebook name in URL')
notebook_frontend.wait_for_condition(
lambda: notebook_name in notebook_frontend.get_page_url(page=EDITOR_PAGE),
timeout=120,
period=5
)
# for notebook_path in ["new_folder/another_new_notebook.ipynb"]:
for notebook_path in ["new_notebook.ipynb", "new_folder/another_new_notebook.ipynb"]:
print(f'[Test] Begin attempts to fill the save dialog input with {notebook_path} and save the notebook')
fill_attempts = 0
check_func = partial(attempt_form_fill_and_save, notebook_path)
notebook_frontend.wait_for_condition(check_func, timeout=900, period=1)

print('[Test] Check notebook name in URL')
try:
notebook_frontend.wait_for_condition(
lambda: notebook_path.split("/")[-1] in notebook_frontend.get_page_url(page=EDITOR_PAGE),
timeout=120,
period=5
)
except:
print(notebook_frontend.get_page_url(page=EDITOR_PAGE))


Loading