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;