ivoryos 1.1.0__py3-none-any.whl → 1.2.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ivoryos might be problematic. Click here for more details.

Files changed (58) hide show
  1. ivoryos/__init__.py +12 -5
  2. ivoryos/routes/api/api.py +5 -58
  3. ivoryos/routes/control/control.py +46 -43
  4. ivoryos/routes/control/control_file.py +4 -4
  5. ivoryos/routes/control/control_new_device.py +10 -10
  6. ivoryos/routes/control/templates/controllers.html +38 -9
  7. ivoryos/routes/control/templates/controllers_new.html +1 -1
  8. ivoryos/routes/data/data.py +81 -60
  9. ivoryos/routes/data/templates/components/step_card.html +9 -3
  10. ivoryos/routes/data/templates/workflow_database.html +10 -4
  11. ivoryos/routes/design/design.py +306 -243
  12. ivoryos/routes/design/design_file.py +42 -31
  13. ivoryos/routes/design/design_step.py +132 -30
  14. ivoryos/routes/design/templates/components/action_form.html +4 -3
  15. ivoryos/routes/design/templates/components/{instrument_panel.html → actions_panel.html} +7 -5
  16. ivoryos/routes/design/templates/components/autofill_toggle.html +8 -12
  17. ivoryos/routes/design/templates/components/canvas.html +5 -14
  18. ivoryos/routes/design/templates/components/canvas_footer.html +5 -1
  19. ivoryos/routes/design/templates/components/canvas_header.html +36 -15
  20. ivoryos/routes/design/templates/components/canvas_main.html +34 -0
  21. ivoryos/routes/design/templates/components/deck_selector.html +8 -10
  22. ivoryos/routes/design/templates/components/edit_action_form.html +16 -7
  23. ivoryos/routes/design/templates/components/instruments_panel.html +66 -0
  24. ivoryos/routes/design/templates/components/modals/drop_modal.html +3 -5
  25. ivoryos/routes/design/templates/components/modals/new_script_modal.html +1 -2
  26. ivoryos/routes/design/templates/components/modals/rename_modal.html +5 -5
  27. ivoryos/routes/design/templates/components/modals/saveas_modal.html +2 -2
  28. ivoryos/routes/design/templates/components/python_code_overlay.html +26 -4
  29. ivoryos/routes/design/templates/components/sidebar.html +12 -13
  30. ivoryos/routes/design/templates/experiment_builder.html +20 -20
  31. ivoryos/routes/execute/execute.py +157 -13
  32. ivoryos/routes/execute/execute_file.py +38 -4
  33. ivoryos/routes/execute/templates/components/tab_bayesian.html +365 -113
  34. ivoryos/routes/execute/templates/components/tab_configuration.html +1 -1
  35. ivoryos/routes/library/library.py +70 -115
  36. ivoryos/routes/library/templates/library.html +27 -19
  37. ivoryos/static/js/action_handlers.js +213 -0
  38. ivoryos/static/js/db_delete.js +23 -0
  39. ivoryos/static/js/script_metadata.js +39 -0
  40. ivoryos/static/js/sortable_design.js +89 -56
  41. ivoryos/static/js/ui_state.js +113 -0
  42. ivoryos/utils/bo_campaign.py +137 -1
  43. ivoryos/utils/db_models.py +14 -5
  44. ivoryos/utils/form.py +4 -9
  45. ivoryos/utils/global_config.py +13 -1
  46. ivoryos/utils/script_runner.py +24 -5
  47. ivoryos/utils/serilize.py +203 -0
  48. ivoryos/utils/task_runner.py +4 -1
  49. ivoryos/version.py +1 -1
  50. {ivoryos-1.1.0.dist-info → ivoryos-1.2.0b1.dist-info}/METADATA +1 -1
  51. {ivoryos-1.1.0.dist-info → ivoryos-1.2.0b1.dist-info}/RECORD +54 -51
  52. ivoryos/routes/design/templates/components/action_list.html +0 -15
  53. ivoryos/routes/design/templates/components/operations_panel.html +0 -43
  54. ivoryos/routes/design/templates/components/script_info.html +0 -31
  55. ivoryos/routes/design/templates/components/scripts.html +0 -50
  56. {ivoryos-1.1.0.dist-info → ivoryos-1.2.0b1.dist-info}/LICENSE +0 -0
  57. {ivoryos-1.1.0.dist-info → ivoryos-1.2.0b1.dist-info}/WHEEL +0 -0
  58. {ivoryos-1.1.0.dist-info → ivoryos-1.2.0b1.dist-info}/top_level.txt +0 -0
@@ -8,114 +8,88 @@ library = Blueprint('library', __name__, template_folder='templates')
8
8
 
9
9
 
10
10
 
11
- @library.route("/edit/<script_name>")
11
+ @library.route("/<string:script_name>", methods=["GET", "POST", "DELETE"])
12
12
  @login_required
13
- def edit_workflow(script_name:str):
13
+ def workflow_script(script_name:str):
14
+ # todo: split this into two routes, one for GET and POST, another for DELETE
14
15
  """
15
- .. :quickref: Database; load workflow script to canvas
16
+ .. :quickref: Workflow Script Database; get, post, delete workflow script
16
17
 
17
- load the selected workflow to the design canvas
18
-
19
- .. http:get:: /database/scripts/edit/<script_name>
18
+ .. http:get:: /<string:script_name>
20
19
 
21
20
  :param script_name: script name
22
21
  :type script_name: str
23
22
  :status 302: redirect to :http:get:`/ivoryos/design/script/`
24
- """
25
- row = Script.query.get(script_name)
26
- script = Script(**row.as_dict())
27
- post_script_file(script)
28
- pseudo_name = session.get("pseudo_deck", "")
29
- off_line = current_app.config["OFF_LINE"]
30
- if off_line and pseudo_name and not script.deck == pseudo_name:
31
- flash(f"Choose the deck with name {script.deck}")
32
- if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
33
- return jsonify({
34
- "script": script.as_dict(),
35
- "python_script": script.compile(),
36
- })
37
- return redirect(url_for('design.experiment_builder'))
38
23
 
24
+ .. http:post:: /<string:script_name>
39
25
 
40
- @library.route("/delete/<script_name>")
41
- @login_required
42
- def delete_workflow(script_name: str):
43
- """
44
- .. :quickref: Database; delete workflow
45
-
46
- delete workflow from database
26
+ :param script_name: script name
27
+ :type script_name: str
28
+ :status 200: json response with success status
47
29
 
48
- .. http:get:: /database/scripts/delete/<script_name>
30
+ .. http:delete:: /<string:script_name>
49
31
 
50
- :param script_name: workflow name
32
+ :param script_name: script name
51
33
  :type script_name: str
52
- :status 302: redirect to :http:get:`/ivoryos/database/scripts/`
34
+ :status 302: redirect to :http:get:`/ivoryos/design/script/`
53
35
 
54
36
  """
55
- Script.query.filter(Script.name == script_name).delete()
56
- db.session.commit()
57
- return redirect(url_for('database.load_from_database'))
37
+ row = Script.query.get(script_name)
38
+ if request.method == "DELETE":
39
+ if not row:
40
+ return jsonify(success=False)
41
+ db.session.delete(row)
42
+ db.session.commit()
43
+ return jsonify(success=True)
44
+ if request.method == "GET":
45
+ if not row:
46
+ return jsonify(success=False)
47
+ script = Script(**row.as_dict())
48
+ post_script_file(script)
49
+ pseudo_name = session.get("pseudo_deck", "")
50
+ off_line = current_app.config["OFF_LINE"]
51
+ if off_line and pseudo_name and not script.deck == pseudo_name:
52
+ flash(f"Choose the deck with name {script.deck}")
53
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
54
+ return jsonify({
55
+ "script": script.as_dict(),
56
+ "python_script": script.compile(),
57
+ })
58
+ return redirect(url_for('design.experiment_builder'))
59
+ if request.method == "POST":
60
+ status = publish()
61
+ return jsonify(status)
62
+ return None
58
63
 
59
64
 
60
- @library.route("/save")
61
- @login_required
62
65
  def publish():
63
- """
64
- .. :quickref: Database; save workflow to database
65
-
66
- save workflow to database
67
-
68
- .. http:get:: /database/scripts/save
69
-
70
- :status 302: redirect to :http:get:`/ivoryos/experiment/build/`
71
- """
72
66
  script = get_script_file()
67
+
68
+ if script.author is None:
69
+ script.author = session.get('user')
73
70
  if not script.name or not script.deck:
74
- flash("Deck cannot be empty, try to re-submit deck configuration on the left panel")
71
+ return {"success": False, "error": "Deck cannot be empty, try to re-submit deck configuration on the left panel"}
75
72
  row = Script.query.get(script.name)
76
73
  if row and row.status == "finalized":
77
- flash("This is a protected script, use save as to rename.")
78
- elif row and not session['user'] == row.author:
79
- flash("You are not the author, use save as to rename.")
80
- else:
81
- db.session.merge(script)
82
- db.session.commit()
83
- flash("Saved!")
84
- return redirect(url_for('design.experiment_builder'))
85
-
86
-
87
- @library.route("/finalize")
88
- @login_required
89
- def finalize():
90
- """
91
- .. :quickref: Database; finalize the workflow
74
+ return {"success": False, "error": "This is a protected script, use save as to rename."}
92
75
 
93
- [protected workflow] prevent saving edited workflow to the same workflow name
94
-
95
- .. http:get:: /finalize
96
-
97
- :status 302: redirect to :http:get:`/ivoryos/experiment/build/`
98
-
99
- """
100
- script = get_script_file()
101
- script.finalize()
102
- if script.name:
76
+ elif row and session.get('user') != row.author:
77
+ return {"success": False, "error": "You are not the author, use save as to rename."}
78
+ else:
103
79
  db.session.merge(script)
104
80
  db.session.commit()
105
- post_script_file(script)
106
- return redirect(url_for('design.experiment_builder'))
81
+ return {"success": True, "message": "Script published successfully"}
107
82
 
108
83
 
109
- @library.route("/get/", strict_slashes=False)
110
- @library.route("/get/<deck_name>")
84
+ @library.get("/", strict_slashes=False)
111
85
  @login_required
112
- def load_from_database(deck_name=None):
86
+ def load_from_database():
113
87
  """
114
- .. :quickref: Database; database page
88
+ .. :quickref: Script Database; database page
115
89
 
116
90
  backend control through http requests
117
91
 
118
- .. http:get:: /database/scripts/<deck_name>
92
+ .. http:get:: /designs/get/<deck_name>
119
93
 
120
94
  :param deck_name: filter for deck name
121
95
  :type deck_name: str
@@ -124,6 +98,7 @@ def load_from_database(deck_name=None):
124
98
  session.pop('edit_action', None) # reset cache
125
99
  query = Script.query
126
100
  search_term = request.args.get("keyword", None)
101
+ deck_name = request.args.get("deck", None)
127
102
  if search_term:
128
103
  query = query.filter(Script.name.like(f'%{search_term}%'))
129
104
  if deck_name is None:
@@ -147,58 +122,38 @@ def load_from_database(deck_name=None):
147
122
  return render_template("library.html", scripts=scripts, deck_list=deck_list, deck_name=deck_name)
148
123
 
149
124
 
150
- @library.route("/rename", methods=['POST'])
151
- @login_required
152
- def edit_run_name():
153
- """
154
- .. :quickref: Database; edit workflow name
155
-
156
- edit the name of the current workflow, won't save to the database
157
-
158
- .. http:post:: database/scripts/rename
159
-
160
- : form run_name: new workflow name
161
- :status 302: redirect to :http:get:`/ivoryos/experiment/build/`
162
-
163
- """
164
- if request.method == "POST":
165
- run_name = request.form.get("run_name")
166
- exist_script = Script.query.get(run_name)
167
- if not exist_script:
168
- script = get_script_file()
169
- script.save_as(run_name)
170
- post_script_file(script)
171
- else:
172
- flash("Script name is already exist in database")
173
- return redirect(url_for("design.experiment_builder"))
174
125
 
175
126
 
176
- @library.route("/save_as", methods=['POST'])
127
+ @library.post("/", strict_slashes=False)
177
128
  @login_required
178
129
  def save_as():
179
130
  """
180
- .. :quickref: Database; save the run name as
131
+ .. :quickref: Script Database; save the script as
181
132
 
182
133
  save the current workflow script as
183
134
 
184
- .. http:post:: /database/scripts/save_as
135
+ .. http:post:: /library/save_as
185
136
 
186
137
  : form run_name: new workflow name
187
- :status 302: redirect to :http:get:`/ivoryos/experiment/build/`
138
+ :status 302: redirect to :http:get:`/ivoryos/design/script/`
188
139
 
189
140
  """
190
141
  if request.method == "POST":
191
142
  run_name = request.form.get("run_name")
143
+ ## TODO: check if run_name is valid
192
144
  register_workflow = request.form.get("register_workflow")
193
- exist_script = Script.query.get(run_name)
194
- if not exist_script:
195
- script = get_script_file()
196
- script.save_as(run_name)
197
- script.registered = register_workflow == "on"
198
- script.author = session.get('user')
199
- post_script_file(script)
200
- publish()
145
+ script = get_script_file()
146
+ script.save_as(run_name)
147
+ script.registered = register_workflow == "on"
148
+ script.author = session.get('user')
149
+ post_script_file(script)
150
+ status = publish()
151
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
152
+ return jsonify(status)
201
153
  else:
202
- flash("Script name is already exist in database")
203
- return redirect(url_for("design.experiment_builder"))
154
+ if status["success"]:
155
+ flash("Script saved successfully")
156
+ else:
157
+ flash(status["error"], "error")
158
+ return redirect(url_for('design.experiment_builder'))
204
159
 
@@ -2,25 +2,28 @@
2
2
 
3
3
  {% block title %}IvoryOS | Design Database{% endblock %}
4
4
  {% block body %}
5
- <div class="database-filter">
6
- {% for deck_name in deck_list %}
7
- {% if deck_name == "ALL" %}<a class="btn btn-secondary" href="{{url_for('library.load_from_database')}}">Back</a>
8
- {% else %}<a class="btn btn-secondary" href="{{url_for('library.load_from_database',deck_name=deck_name)}}">{{deck_name}}</a>
9
- {% endif %}
5
+ <!-- Deck Filter Buttons -->
6
+ <div class="btn-group" role="group">
7
+ {% for deck in deck_list %}
8
+ <a class="btn {% if deck == current_deck_name %}btn-primary{% else %}btn-secondary{% endif %}"
9
+ href="{{ url_for('library.load_from_database', deck_name=deck) }}">
10
+ {{ deck }}
11
+ </a>
10
12
  {% endfor %}
11
-
12
- <form id="search" style="display: inline-block;float: right;" action="{{url_for('library.load_from_database',deck_name=deck_name)}}" method="GET">
13
- <div class="input-group">
14
- <div class="form-outline">
15
- <input type="search" name="keyword" id="keyword" class="form-control" placeholder="Search workflows...">
16
- </div>
17
- <button type="submit" class="btn btn-primary">
18
- <i class="bi bi-search"></i>
19
- </button>
20
- </div>
21
- </form>
22
13
  </div>
23
14
 
15
+ <!-- Search Form -->
16
+ <form id="search" class="d-flex " method="GET" style="display: inline-block;float: right;"
17
+ action="{{ url_for('library.load_from_database', deck_name=current_deck_name or 'ALL') }}">
18
+ <div class="input-group">
19
+ <input type="search" name="keyword" id="keyword" class="form-control"
20
+ placeholder="Search workflows..." value="{{ request.args.get('keyword', '') }}">
21
+ <button type="submit" class="btn btn-primary">
22
+ <i class="bi bi-search"></i>
23
+ </button>
24
+ </div>
25
+ </form>
26
+
24
27
  <table class="table table-hover" id="workflowLibrary">
25
28
  <thead>
26
29
  <tr>
@@ -37,7 +40,7 @@
37
40
  <tbody>
38
41
  {% for script in scripts %}
39
42
  <tr>
40
- <td><a href="{{ url_for('library.edit_workflow', script_name=script.name) }}">{{ script.name }}</a></td>
43
+ <td><a href="{{ url_for('library.workflow_script', script_name=script.name) }}">{{ script.name }}</a></td>
41
44
  <td>{{ script.deck }}</td>
42
45
  <td>{{ script.status }}</td>
43
46
  <td>{{ script.time_created }}</td>
@@ -47,7 +50,12 @@
47
50
  <td>
48
51
  {#not workflow.status == "finalized" or#}
49
52
  {% if session['user'] == 'admin' or session['user'] == script.author %}
50
- <a href="{{ url_for('library.delete_workflow', script_name=script.name) }}">delete</a>
53
+ <a href="#"
54
+ class="text-danger"
55
+ data-delete-url="{{ url_for('library.workflow_script', script_name=script.name) }}"
56
+ onclick="deleteWorkflow(this); return false;">
57
+ Delete
58
+ </a>
51
59
  {% else %}
52
60
  <a class="disabled-link">delete</a>
53
61
  {% endif %}
@@ -79,5 +87,5 @@
79
87
  <a class="page-link" href="{{ url_for('library.load_from_database', page=scripts.next_num) }}">Next</a>
80
88
  </div>
81
89
  </div>
82
-
90
+ <script src="{{ url_for('static', filename='js/db_delete.js') }}"></script>
83
91
  {% endblock %}
@@ -0,0 +1,213 @@
1
+
2
+
3
+
4
+
5
+ function saveWorkflow(link) {
6
+ const url = link.dataset.postUrl;
7
+
8
+ fetch(url, {
9
+ method: 'POST',
10
+ headers: {
11
+ 'Content-Type': 'application/json'
12
+ }
13
+ })
14
+ .then(res => res.json())
15
+ .then(data => {
16
+ if (data.success) {
17
+ // flash a success message
18
+ flash("Workflow saved successfully", "success");
19
+ window.location.reload(); // or update the UI dynamically
20
+ } else {
21
+ alert("Failed to save workflow: " + data.error);
22
+ }
23
+ })
24
+ .catch(err => {
25
+ console.error("Save error:", err);
26
+ alert("Something went wrong.");
27
+ });
28
+ }
29
+
30
+
31
+ function updateInstrumentPanel(link) {
32
+ const url = link.dataset.getUrl;
33
+ fetch(url)
34
+ .then(res => res.json())
35
+ .then(data => {
36
+ if (data.html) {
37
+ document.getElementById("sidebar-wrapper").innerHTML = data.html;
38
+ initializeDragHandlers()
39
+ }
40
+ })
41
+ }
42
+
43
+ function addMethodToDesign(event, form) {
44
+ event.preventDefault(); // Prevent default form submission
45
+
46
+ const formData = new FormData(form);
47
+
48
+ fetch(form.action, {
49
+ method: 'POST',
50
+ body: formData
51
+ })
52
+ .then(response => response.json())
53
+ .then(data => {
54
+ if (data.success) {
55
+ updateActionCanvas(data.html);
56
+ hideModal();
57
+ } else {
58
+ alert("Failed to add method: " + data.error);
59
+ }
60
+ })
61
+ .catch(error => console.error('Error:', error));
62
+ }
63
+
64
+
65
+ function updateActionCanvas(html) {
66
+ document.getElementById("canvas-action-wrapper").innerHTML = html;
67
+ initializeCanvas(); // Reinitialize canvas functionality
68
+ document.querySelectorAll('#pythonCodeOverlay pre code').forEach((block) => {
69
+ hljs.highlightElement(block);
70
+ });
71
+ }
72
+
73
+
74
+ let lastFocusedElement = null;
75
+
76
+
77
+ function hideModal() {
78
+ if (document.activeElement) {
79
+ document.activeElement.blur();
80
+ }
81
+ $('#dropModal').modal('hide');
82
+ if (lastFocusedElement) {
83
+ lastFocusedElement.focus(); // Return focus to the triggering element
84
+ }
85
+ }
86
+
87
+ function submitEditForm(event) {
88
+ event.preventDefault();
89
+ const form = event.target;
90
+ const formData = new FormData(form);
91
+
92
+ fetch(form.action, {
93
+ method: 'POST',
94
+ body: formData
95
+ })
96
+ .then(response => response.text())
97
+ .then(html => {
98
+ if (html) {
99
+ // Update only the action list
100
+ updateActionCanvas(html);
101
+
102
+ if (previousHtmlState) {
103
+ document.getElementById('instrument-panel').innerHTML = previousHtmlState;
104
+ previousHtmlState = null; // Clear the stored state
105
+ }
106
+ }
107
+ })
108
+ .catch(error => {
109
+ console.error('Error:', error);
110
+ });
111
+ }
112
+
113
+ function clearDraft() {
114
+ fetch(scriptDeleteUrl, {
115
+ method: "DELETE",
116
+ headers: {
117
+ "Content-Type": "application/json",
118
+ },
119
+ })
120
+ .then(res => res.json())
121
+ .then(data => {
122
+ if (data.success) {
123
+ window.location.reload();
124
+ } else {
125
+ alert("Failed to clear draft");
126
+ }
127
+ })
128
+ .catch(error => console.error("Failed to clear draft", error));
129
+ }
130
+
131
+
132
+
133
+
134
+ let previousHtmlState = null; // Store the previous state
135
+
136
+ function duplicateAction(uuid) {
137
+ if (!uuid) {
138
+ console.error('Invalid UUID');
139
+ return;
140
+ }
141
+
142
+ fetch(scriptStepDupUrl.replace('0', uuid), {
143
+ method: 'POST',
144
+ headers: {
145
+ 'Content-Type': 'application/json'
146
+ }
147
+ })
148
+
149
+ .then(response => response.text())
150
+ .then(html => {
151
+ updateActionCanvas(html);
152
+ })
153
+ .catch(error => console.error('Error:', error));
154
+ }
155
+
156
+ function editAction(uuid) {
157
+ if (!uuid) {
158
+ console.error('Invalid UUID');
159
+ return;
160
+ }
161
+
162
+ // Save current state before fetching new content
163
+ previousHtmlState = document.getElementById('instrument-panel').innerHTML;
164
+
165
+ fetch(scriptStepUrl.replace('0', uuid), {
166
+ method: 'GET',
167
+ headers: {
168
+ 'Content-Type': 'application/json'
169
+ }
170
+ })
171
+ .then(response => response.text())
172
+ .then(html => {
173
+ document.getElementById('instrument-panel').innerHTML = html;
174
+
175
+ // Add click handler for back button
176
+ document.getElementById('back').addEventListener('click', function(e) {
177
+ e.preventDefault();
178
+ if (previousHtmlState) {
179
+ document.getElementById('instrument-panel').innerHTML = previousHtmlState;
180
+ previousHtmlState = null; // Clear the stored state
181
+ }
182
+ });
183
+ })
184
+ .catch(error => console.error('Error:', error));
185
+ }
186
+
187
+
188
+
189
+ function deleteAction(uuid) {
190
+ if (!uuid) {
191
+ console.error('Invalid UUID');
192
+ return;
193
+ }
194
+
195
+ fetch(scriptStepUrl.replace('0', uuid), {
196
+ method: 'DELETE',
197
+ headers: {
198
+ 'Content-Type': 'application/json'
199
+ }
200
+ })
201
+ .then(response => response.text())
202
+ .then(html => {
203
+ // Find the first list element's content and replace it
204
+ updateActionCanvas(html);
205
+ })
206
+ .catch(error => console.error('Error:', error));
207
+ }
208
+
209
+
210
+
211
+
212
+
213
+
@@ -0,0 +1,23 @@
1
+
2
+ function deleteWorkflow(link) {
3
+ const url = link.dataset.deleteUrl;
4
+
5
+ fetch(url, {
6
+ method: 'DELETE',
7
+ headers: {
8
+ 'Content-Type': 'application/json'
9
+ }
10
+ })
11
+ .then(res => res.json())
12
+ .then(data => {
13
+ if (data.success) {
14
+ window.location.reload(); // or remove the row dynamically
15
+ } else {
16
+ alert("Failed to delete workflow: " + data.error);
17
+ }
18
+ })
19
+ .catch(err => {
20
+ console.error("Delete error:", err);
21
+ alert("Something went wrong.");
22
+ });
23
+ }
@@ -0,0 +1,39 @@
1
+ function editScriptName(event) {
2
+ event.preventDefault(); // Prevent full form submission
3
+ const newName = document.getElementById("new-name").value;
4
+ fetch(scriptMetaUrl, {
5
+ method: "PATCH",
6
+ headers: {
7
+ "Content-Type": "application/json",
8
+ },
9
+ body: JSON.stringify({name: newName})
10
+ })
11
+ .then(res => res.json())
12
+ .then(data => {
13
+ if (data.success) {
14
+ window.location.reload(); // or update the title on page directly
15
+ } else {
16
+ alert("Failed to rename script");
17
+ }
18
+ })
19
+ .catch(error => console.error("Failed to rename script", error));
20
+ }
21
+
22
+ function lockScriptEditing() {
23
+ fetch(scriptMetaUrl, {
24
+ method: "PATCH",
25
+ headers: {
26
+ "Content-Type": "application/json",
27
+ },
28
+ body: JSON.stringify({ status: "finalized" })
29
+ })
30
+ .then(res => res.json())
31
+ .then(data => {
32
+ if (data.success) {
33
+ window.location.reload();
34
+ } else {
35
+ alert("Failed to update script status");
36
+ }
37
+ })
38
+ .catch(error => console.error("Failed to update script status", error));
39
+ }