diff --git a/src/gui/src/UI/Dashboard/TabFiles.js b/src/gui/src/UI/Dashboard/TabFiles.js
index d3f615e0f..5aad4c400 100644
--- a/src/gui/src/UI/Dashboard/TabFiles.js
+++ b/src/gui/src/UI/Dashboard/TabFiles.js
@@ -29,6 +29,7 @@ import truncate_filename from '../../helpers/truncate_filename.js';
import update_title_based_on_uploads from '../../helpers/update_title_based_on_uploads.js';
import item_icon from '../../helpers/item_icon.js';
import new_context_menu_item from '../../helpers/new_context_menu_item.js';
+import publish_as_website from '../../helpers/publish_as_website.js';
import ContextMenuModal from './ContextMenu/ContextMenu.js';
import UIItemPropertiesModal from './UIItemPropertiesModal.js';
@@ -3782,6 +3783,7 @@ const TabFiles = {
if ( ! targetPath ) return [];
const isTrashFolder = targetPath === window.trash_path;
+ const isTrashedPath = targetPath.startsWith(`${window.trash_path}/`);
const items = [];
// New submenu (folder, text document, etc.) - not available in Trash
@@ -3983,6 +3985,45 @@ const TabFiles = {
});
}
+ // Publish As Website and Properties act on the folder the menu was
+ // opened on, not on the listing's selection. The filesystem root isn't
+ // a real fsentry, and Trash itself only offers Empty Trash — same as
+ // the desktop's folder menus.
+ if ( targetPath !== '/' && ! isTrashFolder ) {
+ const targetName = path.basename(targetPath);
+
+ items.push('-');
+
+ if ( ! isTrashedPath ) {
+ items.push({
+ html: i18n('publish_as_website'),
+ onClick: async function () {
+ // Publishing works off the path, but the uid is what
+ // refreshes the folder's website badge if its row is on
+ // screen — the dashboard tracks paths, so look it up.
+ let uid;
+ try {
+ uid = (await puter.fs.stat({ path: targetPath, consistency: 'eventual' }))?.uid;
+ } catch ( err ) {
+ // Badge refresh is best-effort; publish still works.
+ }
+ await publish_as_website({ uid, name: targetName, path: targetPath });
+ },
+ });
+ }
+
+ items.push({
+ html: i18n('properties'),
+ onClick: function () {
+ UIItemPropertiesModal({
+ name: targetName,
+ path: targetPath,
+ $container: _this.$el_window,
+ });
+ },
+ });
+ }
+
return items;
},
diff --git a/src/gui/src/UI/Dashboard/UIItemPropertiesModal.js b/src/gui/src/UI/Dashboard/UIItemPropertiesModal.js
index 3c604d30c..dc619a6bf 100644
--- a/src/gui/src/UI/Dashboard/UIItemPropertiesModal.js
+++ b/src/gui/src/UI/Dashboard/UIItemPropertiesModal.js
@@ -50,7 +50,7 @@ function addRow ($list, labelHtml, valueHtml) {
* @param {Object} opts
* @param {string} opts.name - Display name of the item
* @param {string} opts.path - Full path of the item
- * @param {string} opts.uid - UID of the item
+ * @param {string} [opts.uid] - UID of the item; looked up from the path when omitted
* @param {jQuery} [opts.$container] - Element to append the overlay to (defaults to
)
* @returns {{ close: () => void }}
*/
@@ -121,8 +121,12 @@ export default function UIItemPropertiesModal ({ name, path: item_path, uid, $co
const $list = $overlay.find('.item-props-list');
const $versions = $overlay.find('.item-props-versions');
+ // The folder-background menu knows only the path it was opened on, so
+ // address the entry by uid when we have one and fall back to the path.
+ let item_uid = uid;
+
puter.fs.stat({
- uid,
+ ...(uid ? { uid } : { path: item_path }),
returnSubdomains: true,
returnPermissions: true,
returnVersions: true,
@@ -131,6 +135,8 @@ export default function UIItemPropertiesModal ({ name, path: item_path, uid, $co
success: async (fsentry) => {
if ( closed ) return;
+ item_uid = item_uid ?? fsentry.uid ?? fsentry.id;
+
// Icon
try {
const icon = await item_icon(fsentry);
@@ -214,7 +220,7 @@ export default function UIItemPropertiesModal ({ name, path: item_path, uid, $co
puter.hosting.update($link.attr('data-subdomain'), null).then(() => {
$overlay.find(`.item-props-website-entry[data-uuid="${$link.attr('data-uuid')}"]`).remove();
// Remove the website badge from every row for this item.
- $(`.item[data-uid="${uid}"]`).find('.item-has-website-badge').fadeOut(200);
+ $(`.item[data-uid="${item_uid}"]`).find('.item-has-website-badge').fadeOut(200);
});
});
},
diff --git a/src/gui/src/UI/UIItem.js b/src/gui/src/UI/UIItem.js
index 1a12304c9..9919100a3 100644
--- a/src/gui/src/UI/UIItem.js
+++ b/src/gui/src/UI/UIItem.js
@@ -17,7 +17,6 @@
* along with this program. If not, see .
*/
-import UIWindowPublishWebsite from './UIWindowPublishWebsite.js';
import UIWindowItemProperties from './UIWindowItemProperties.js';
import UIWindowSaveAccount from './UIWindowSaveAccount.js';
import UIPopover from './UIPopover.js';
@@ -29,6 +28,7 @@ import path from '../lib/path.js';
import truncate_filename from '../helpers/truncate_filename.js';
import launch_app from '../helpers/launch_app.js';
import open_item from '../helpers/open_item.js';
+import publish_as_website from '../helpers/publish_as_website.js';
import mime from '../lib/mime.js';
import { isWeblinkName, weblinkChangeIconMenuItem } from '../helpers/weblink.js';
@@ -1280,25 +1280,11 @@ async function UIItem (options) {
html: i18n('publish_as_website'),
disabled: !options.is_dir,
onClick: async function () {
- if ( window.require_email_verification_to_publish_website ) {
- if ( window.user.is_temp &&
- !await UIWindowSaveAccount({
- send_confirmation_code: true,
- message: 'Please create an account to proceed.',
- window_options: {
- backdrop: true,
- close_on_backdrop_click: false,
- },
- }) )
- {
- return;
- }
- else if ( !window.user.email_confirmed && !await UIWindowEmailConfirmationRequired() )
- {
- return;
- }
- }
- UIWindowPublishWebsite(options.uid, $(el_item).attr('data-name'), $(el_item).attr('data-path'));
+ await publish_as_website({
+ uid: options.uid,
+ name: $(el_item).attr('data-name'),
+ path: $(el_item).attr('data-path'),
+ });
},
});
diff --git a/src/gui/src/UI/UIWindow.js b/src/gui/src/UI/UIWindow.js
index 66c0550d2..7f7051de2 100644
--- a/src/gui/src/UI/UIWindow.js
+++ b/src/gui/src/UI/UIWindow.js
@@ -22,13 +22,11 @@ import UIContextMenu from './UIContextMenu.js';
import path from '../lib/path.js';
import UITaskbarItem from './UITaskbarItem.js';
import UIWindowLogin from './UIWindowLogin.js';
-import UIWindowPublishWebsite from './UIWindowPublishWebsite.js';
import UIWindowItemProperties from './UIWindowItemProperties.js';
import new_context_menu_item from '../helpers/new_context_menu_item.js';
import refresh_item_container from '../helpers/refresh_item_container.js';
-import UIWindowSaveAccount from './UIWindowSaveAccount.js';
-import UIWindowEmailConfirmationRequired from './UIWindowEmailConfirmationRequired.js';
import launch_app from '../helpers/launch_app.js';
+import publish_as_website from '../helpers/publish_as_website.js';
import item_icon from '../helpers/item_icon.js';
@@ -2569,25 +2567,11 @@ async function UIWindow (options) {
html: i18n('publish_as_website'),
disabled: !options.is_dir,
onClick: async function () {
- if ( window.require_email_verification_to_publish_website ) {
- if ( window.user.is_temp &&
- !await UIWindowSaveAccount({
- send_confirmation_code: true,
- message: i18n('save_account_to_publish'),
- window_options: {
- backdrop: true,
- close_on_backdrop_click: false,
- },
- }) )
- {
- return;
- }
- else if ( !window.user.email_confirmed && !await UIWindowEmailConfirmationRequired() )
- {
- return;
- }
- }
- UIWindowPublishWebsite($(el_window).attr('data-uid'), $(el_window).attr('data-name'), $(el_window).attr('data-path'));
+ await publish_as_website({
+ uid: $(el_window).attr('data-uid'),
+ name: $(el_window).attr('data-name'),
+ path: $(el_window).attr('data-path'),
+ });
},
});
// -------------------------------------------
diff --git a/src/gui/src/helpers/generate_file_context_menu.js b/src/gui/src/helpers/generate_file_context_menu.js
index a0091ebd7..a98b9f249 100644
--- a/src/gui/src/helpers/generate_file_context_menu.js
+++ b/src/gui/src/helpers/generate_file_context_menu.js
@@ -19,11 +19,11 @@
import UIAlert from '../UI/UIAlert.js';
-import UIWindowPublishWebsite from '../UI/UIWindowPublishWebsite.js';
import UIWindowItemProperties from '../UI/UIWindowItemProperties.js';
import UIWindowSaveAccount from '../UI/UIWindowSaveAccount.js';
import UIWindowEmailConfirmationRequired from '../UI/UIWindowEmailConfirmationRequired.js';
import UIWindowPublishWorker from '../UI/UIWindowPublishWorker.js';
+import publish_as_website from './publish_as_website.js';
import open_item from './open_item.js';
import launch_app from './launch_app.js';
import path from '../lib/path.js';
@@ -101,23 +101,11 @@ const generate_file_context_menu = async function (options) {
html: i18n('publish_as_website'),
disabled: !fsentry.is_dir || fsentry.has_website,
onClick: async function () {
- if ( window.require_email_verification_to_publish_website ) {
- if ( window.user.is_temp &&
- !await UIWindowSaveAccount({
- send_confirmation_code: true,
- message: 'Please create an account to proceed.',
- window_options: {
- backdrop: true,
- close_on_backdrop_click: false,
- },
- }) ) {
- return;
- }
- else if ( !window.user.email_confirmed && !await UIWindowEmailConfirmationRequired() ) {
- return;
- }
- }
- UIWindowPublishWebsite(fsentry.uid, $(el_item).attr('data-name'), $(el_item).attr('data-path'));
+ await publish_as_website({
+ uid: fsentry.uid,
+ name: $(el_item).attr('data-name'),
+ path: $(el_item).attr('data-path'),
+ });
},
});
}
diff --git a/src/gui/src/helpers/publish_as_website.js b/src/gui/src/helpers/publish_as_website.js
new file mode 100644
index 000000000..b36eea71a
--- /dev/null
+++ b/src/gui/src/helpers/publish_as_website.js
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024-present Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import UIWindowPublishWebsite from '../UI/UIWindowPublishWebsite.js';
+import UIWindowSaveAccount from '../UI/UIWindowSaveAccount.js';
+import UIWindowEmailConfirmationRequired from '../UI/UIWindowEmailConfirmationRequired.js';
+
+/**
+ * Opens the "Publish as website" flow for a directory, first walking the user
+ * through account creation and email confirmation when the deployment requires
+ * a verified email to publish. Returns without publishing if the user backs out
+ * of either step.
+ *
+ * @param {Object} dir - The directory to publish
+ * @param {string} dir.uid - UID of the directory
+ * @param {string} dir.name - Display name of the directory
+ * @param {string} dir.path - Full path of the directory
+ * @returns {Promise}
+ */
+const publish_as_website = async function ({ uid, name, path }) {
+ if ( window.require_email_verification_to_publish_website ) {
+ if ( window.user.is_temp &&
+ !await UIWindowSaveAccount({
+ send_confirmation_code: true,
+ message: i18n('save_account_to_publish'),
+ window_options: {
+ backdrop: true,
+ close_on_backdrop_click: false,
+ },
+ }) )
+ {
+ return;
+ }
+ else if ( !window.user.email_confirmed && !await UIWindowEmailConfirmationRequired() )
+ {
+ return;
+ }
+ }
+
+ await UIWindowPublishWebsite(uid, name, path);
+};
+
+export default publish_as_website;