- Notifications
You must be signed in to change notification settings - Fork5.5k
Add save as menu option#3289
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Conversation
takluyver commentedFeb 2, 2018
I would expect save-as to work without needing any changes to the REST API or the server. It should update the path for the notebook file in the frontend, and then do a regular save (i.e. a PUT request to the URL of the new file). This is in line with how 'save as' works in most applications: it changes the file path you're working on, so if you open A and then 'save as' B, subsequent saves will write B until you use 'save as' again. Some applications have a separate 'save a copy' action, which lets you write file B but keep working on A. I don't think that's a priority to add to the notebook - not many other apps have it. |
Madhu94 commentedFeb 2, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
That's how I started but I thinkthis line stopped me. I'll make the changes, thanks |
takluyver commentedFeb 2, 2018
Yup, I'd just send the whole contents again (for now, every save does that). The notebook may have changed since it was last saved, so copying the file we saved before may not be right. |
f97e26e to50d6297CompareMadhu94 commentedFeb 13, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
@takluyver Don't you think that if "Save As" behaves almost like "Save", the user could accidentally overwrite a notebook, if they are not careful? I guess "save as" should do an additional check and warn the user that the file exists.. |
takluyver commentedFeb 14, 2018
Yes, ideally if the user tries to 'save as' over an existing file, they should be warned about that. |
0610bd5 to21b7710CompareMadhu94 commentedMar 11, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
@takluyver Now the user is asked to confirm if they want to overwrite, if a notebook exists with the same name. Let me know if these changes are okay. |
takluyver commentedMar 13, 2018
I think the frontend stuff (the dialog, updating the URL, etc.) should be able to mostly use the code we already have in |
Madhu94 commentedMar 13, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Did you mean for reusing the dialog, I should use a flag ? |
takluyver commentedMar 13, 2018
Yep, so it would be called like this: notebook.save_widget.rename_notebook({notebook:notebook,copy_file:true}); |
21b7710 to5fdf8c2CompareMadhu94 commentedMar 17, 2018 via email
I've factored out the common portions for "save as" and rename. I know thisisn't what you had in mind, but using a flag brought a lot of "if -else"clauses within the click handler of rename. I'll do that if you feelstrongly about it though.Re the feature itself, do you think it should take paths relative tocurrent directory and not the notebook root ? That's how creating a newnotebook works.. …On 13-Mar-2018 10:23 PM, "Thomas Kluyver" ***@***.***> wrote: Yep, so it would be called like this: notebook.save_widget.rename_notebook({notebook: notebook, copy_file: true}); — You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub <#3289 (comment)>, or mute the thread <https://github.com/notifications/unsubscribe-auth/AImSDAaCdVPTHyAa7-e6hvdtyyMv3dtWks5td_lzgaJpZM4R2G_h> . |
takluyver commentedMar 19, 2018
From a user perspective, I think the starting point should be the directory the notebook is currently saved in. The REST API should always work with paths from the notebook root, though. |
| }; | ||
| Notebook.prototype.save_notebook_as=function(){ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I'd probably put this method intosavewidget.js because of the symmetry with rename.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Will do!
| Notebook.prototype.save_notebook_as=function(){ | ||
| // If current notebook has some changes, save them | ||
| if(this.writable&&this.dirty){ | ||
| this.save_notebook(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This is to save the current notebook before navigating away. We did agree that "Save as" would open the newly created notebook (Right? Did I misunderstand?), so I thought we should try to save the current notebook before moving away. I think "make a copy" and "trust" do this too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This is to save the current notebook before navigating away. We did agree that "Save as" would open the newly created notebook (Right? Did I misunderstand?), so I thought we should try to save the current notebook before moving away. I think "make a copy" and "trust" do this too.
If you do open the new notebook, then this will likely be almost identical to "Make a Copy", Should we have "Make a copy" take a parameter which is the new name/path ?
| dialog_title:i18n.msg._('Save As'), | ||
| message:i18n.msg._('Enter a notebook path relative to notebook dir'), | ||
| message_classname:'save-message', | ||
| button_name:'Save', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This should probably be translatable.
| returnthat.contents.save(nb_path,model) | ||
| .then(function(data){ | ||
| vardir_path=utils.get_body_data("baseUrl"); | ||
| window.open(utils.url_path_join(dir_path,"notebooks",data.path),'_self'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
There's already code to deal with updating the address bar and other details when the notebook is renamed, based on thenotebook_renamed.Notebook event. Instead of calling window.open(), we should make sure that event fires, as on this line:
| that.events.trigger('notebook_renamed.Notebook',json); |
We should also ensure that the session is updated - that links a kernel to a filename. See the line above the one I just linked to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I'm not sure if triggering event will cause the new notebook to open up...I'll check and get back.
| */ | ||
| SaveWidget.prototype._nb_name_dialog=function(options){ | ||
| varthat=this; | ||
| vardialog_template=_.template(`<p class="<%= classname %>"><%= msg %></p> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Neat! I assume the_.template() makes it translatable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
No, I'm sorry. Underscore templates are just plain html templates. This was just an attempt to share some of the ui code for "save as" and "rename".
Madhu94 commentedMar 21, 2018
I.e. "save as" will be restricted to the current dir + all subdirectories therein ? |
Madhu94 commentedApr 15, 2018
@takluyver I'm sorry, this PR "fell through the cracks". I will try to wind this up in the next few days |
Madhu94 commentedApr 19, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I'm really, really sorry, but I'm not able to understand why the above changes are requested, hence a lot of delay. I'll come out and place my concerns here -
Save As
Please let me know why you think "save as" and "rename" should be implemented the same. Thanks for your patience. |
takluyver commentedApr 19, 2018
Let me try to put those as I see them: Rename
Save as
Does that make it clearer? I think maybe the disconnect is that you're talking about opening the new notebook after 'save as' - are you thinking it would open a second tab, so you would have both notebooks open? It's an interesting idea, but what I describe is the way lots of familiar programs do 'save as' - you have just one editor open afterwards, and it's associated with the new file. |
Madhu94 commentedApr 20, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I see. The disconnect was because I implemented "save as" to work with any path relative to the notebook's root_dir, and I don't think that can be implemented with a rename. I guess that isn't relevant now as we later agreed to implement save asonly in the current directory. Thanks a lot for the expanation, I will implement "Save As" as rename + save. |
notebook/templates/notebook.html Outdated
| data-ws-url="{{ws_url | urlencode}}" | ||
| data-notebook-name="{{notebook_name | urlencode}}" | ||
| data-notebook-path="{{notebook_path | urlencode}}" | ||
| data-server-root="{{server_root}}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I would like to try that without exposingserver_root it is potentially sensitive informations, and we tried out best to not expose it anywhere in the UI. The root is usually represented as the Home icon.
As far as I can tell having this information is only for display purpose, so I would just stay with the home icon.
takluyverApr 21, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I actually disagree. I don't thinkserver_root is really that sensitive, and it's probably not hard to work out from the list of files, which is already accessible. The last time we thought about this was before we enabled authentication by default; now that we've done that, I think we can be slightly more relaxed about showing this kind of information.
We've been using the home icon for lack of anything better, but I don't think it's great UI:
- It's easy to confuse with your home directory, especially as the two are sometimes but not always the same. File managers frequently use a very similar icon for your home directory, e.g. in Gnome:

- It obscures where the files are actually stored, if you want to go and work with them outside the notebook. At one point Anaconda's GUI launcher would create a directory like 'Notebooks' to run the server in, which is hard to discover from inside.
We should make sure it comes from the contents manager and works if it's empty, to allow for non-filesystem contents managers. But I think the benefits of showing it outweigh the disadvantages.
Carreau commentedApr 20, 2018
I agree with your plan, and think that with this approach we have something really close to what "Make a copy" does, and it would be nice to get both functionality closer. I think that "Save as..." may be more familiar than "Make a copy", and we could try to see if "Make a copy" could be removed in the longer term. |
Madhu94 commentedApr 20, 2018 via email
I'm a little confused nowI think it would be easier to decide what to do if we fix on how "Save As"should behave - save a new notebook in the current directory or anywhererelative to root_dir ?If we decide that it is the current directory, I agree with@takluyver thatthis feature is a lot like renameIf not, rename may not be identical to Save As. …On Sat 21 Apr, 2018, 2:33 AM Matthias Bussonnier, ***@***.***> wrote: I agree with your plan, and think that with this approach we have something really close to what "Make a copy" does, and it would be nice to get both functionality closer. I think that "Save as..." may be more familiar than "Make a copy", and we could try to see if "Make a copy" could be removed in the longer term. — You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub <#3289 (comment)>, or mute the thread <https://github.com/notifications/unsubscribe-auth/AImSDGShpd_A3tnmSeExIbTeQ3cPmMz2ks5tqk0ggaJpZM4R2G_h> . |
takluyver commentedApr 21, 2018
Ah, my bad. Rename and move are the same operation in the REST API, and I thought we'd made the GUI so that rename could move it to a different folder, but I was wrong about that.
I think it should be able to save anywhere under the server root directory, but in terms of the UI, it should work relative to the directory the notebook is already in. So if I start the notebook server at |
Madhu94 commentedApr 29, 2018
So, if the user types |
5fdf8c2 tod3b29edCompareMadhu94 commentedApr 29, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I've changed it back to the original implementation which behaved a little like "Make a copy". Even deciding to make it relative to current dir should be as easy as appending |
takluyver commentedApr 29, 2018
Yes... but if possible, could we make it so that when the dialog appears, |
| }; | ||
| returnthat.contents.save(nb_path,model) | ||
| .then(function(data){ | ||
| window.open(data.path,'_self'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Here I think we should use the same functions that rename uses to update the session and the page:
notebook/notebook/static/notebook/js/notebook.js
Lines 2984 to 2985 in2aca6f9
| that.session.rename_notebook(json.path); | |
| that.events.trigger('notebook_renamed.Notebook',json); |
Madhu94 commentedApr 29, 2018
I think I got the behavior you wanted now. (Although the save as method has become a callback+promise hell...) |
takluyver left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
The code and the functionality are looking good now, but I found a few minor UI issues.
I'd also like to have a basic Selenium test for this - see this folder:https://github.com/jupyter/notebook/tree/master/notebook/tests/selenium
| Notebook.prototype.save_notebook_as=function(){ | ||
| // If current notebook has some changes, save them, or the copied notebook won't have them. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Is this still needed? We're sending a new copy of the notebook to the new name, so I don't think we need to save it at the old name again first.
| ).append($('<label />').attr('for','save-as-dialog') | ||
| .html('<i class="fa fa-home"></i>') | ||
| ).append( | ||
| $('<input/>').attr('type','text').attr('size','25') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This input area doesn't show up well for me. I think you're missing theform-control class that the rename dialog uses:
| $('<input/>').attr('type','text').attr('size','25').addClass('form-control') |
Also, I hope we can get rid of the inline styling a couple of lines below.
| returnfalse; | ||
| } | ||
| }); | ||
| d.find('input[type="text"]').focus().select(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This currently selects the path pre-filled in the input, which is not ideal. Can we make it put the cursor at the end of the input already there?
notebook/templates/notebook.html Outdated
| <ahref="#">{% trans %}Make a Copy...{% endtrans %}</a></li> | ||
| <liid="save_notebook_as" | ||
| title="{% trans %}Save a copy of the notebook's contents and start a new kernel{% endtrans %}"> | ||
| <ahref="#">{% trans %}Save as{% endtrans %}</a></li> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This label should end... to indicate that it brings up a dialog rather than acting immediately.
576d3ea to84e565cCompareMadhu94 commentedMay 13, 2018
I added one. Also took care of the css issues. |
Madhu94 commentedJun 13, 2018
Hi@takluyver@Carreau, any updates here ? |
| $('.save-message').html(`<span style='color:red;'>${msg}</span>`); | ||
| }); | ||
| } | ||
| that.contents.get(nb_path,{type:'notebook'}).then(function(data){ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This should at least usecontent: false to avoid getting the full content of the other notebook if it does exist. It's also not great in that it assumes any error getting a model means that the file is not there.
The right way to do this is probably to add a parameter when saving, to ask the server to throw an error if the filename already exists. Maybe this way of doing it is good enough for now, though. I'd like to get some other opinions on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This should at least use content: false to avoid getting the full content of the other notebook if it does exist.
This is done, thank you.
It's also not great in that it assumes any error getting a model means that the file is not there.
I will try to think of a better way. Maybe we go back to something like my original implementation which used a flag to toggle between copy/save on the server..
if it existsDon't get entire contents of the notebooks, while checkingif it exists
Madhu94 commentedJun 18, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
What do you think of adding a HEAD HTTP method to the contents handler? This would simply call contents_manager.file_exists or contents_manager.dir_exists. (I'm not sure if things like this are "done") It might be better than adding a flag while saving, I don't like how that looks - |
takluyver commentedJun 18, 2018
takluyver commentedJun 18, 2018
@Madhu94 It seems to be failing the tests, if you could take a look. |
Madhu94 commentedJun 18, 2018
".CodeMirror-code" still did not exist" => I thought this was some transient Travis bug, will check |
takluyver commentedJun 18, 2018
I think that error can mean it's failing to load the notebook page - it waits for a codemirror block to exist to check that the notebook is loading correctly. The fact that it's doing that on all frontend tests suggests that there's a real problem somewhere (though it might be that something else changed at the same time). |
ES6 syntax is not available to us
minrk commentedJun 20, 2018
I tracked down the test failure to some ES6 syntax in the new javascript. Removing that should let the tests pass. |
takluyver commentedJun 20, 2018
Indeed it does :-). Thanks Min, and thanks@Madhu94 for your patience with this. Merging now. |
takluyver commentedJun 20, 2018
Heads up@mpacer that this is another change for 5.6. |
Madhu94 commentedJun 20, 2018 via email
Thanks@minrk, I was having a lot of trouble with that. Thanks for allyour help, @taklyuver …On Wed 20 Jun, 2018, 4:21 PM Thomas Kluyver, ***@***.***> wrote: Indeed it does :-). Thanks Min, and thanks@Madhu94 <https://github.com/Madhu94> for your patience with this. Merging now. — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <#3289 (comment)>, or mute the thread <https://github.com/notifications/unsubscribe-auth/AImSDP-dMzwgQozXqYk9ynoy6eIxXVhoks5t-ik1gaJpZM4R2G_h> . |
rachelgshaffer commentedOct 3, 2018
@Madhu94@takluyver quick question... I am curious about the implementation here. The tooltip indicates that it starts a new session but seems the intent was to associate the new name with the same kernel session. I wanted to double check on the actual behavior: does it keep the same kernel or start a new one? |
takluyver commentedOct 22, 2018
@rachelgshaffer it should be keeping the same session and changing the filename associated with it. The 'start a new kernel' language in the tooltip is misleading copy-pasta, I think. Do you want to make a PR to fix that? |
Uh oh!
There was an error while loading.Please reload this page.
Trying toclose#2816
I'd like some quick, initial feedback on this.
I am checking the tests. The UI needs some work too (maybe a directory picker instead of a text box?)
Thanks.