feat(dashboard): add Publish as website and Properties to folder menu

The Files tab's folder-background context menu (empty listing area,
sidebar folders, breadcrumb segments) now offers Publish as website and
Properties for the folder the menu was opened on, matching the desktop's
folder menus. Both are omitted at the filesystem root and in Trash, and
Publish is omitted inside trashed folders.

The properties modal can now resolve an entry by path when the caller
has no uid, which the folder-background menu doesn't.

The publish gate (account creation + email confirmation) was duplicated
in three places, so it moves to a publish_as_website helper that all
call sites now share.
This commit is contained in:
jelveh
2026-08-12 08:50:03 -07:00
parent e8236a100c
commit ea63c3093e
6 changed files with 127 additions and 63 deletions
+41
View File
@@ -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;
},
@@ -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 <body>)
* @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);
});
});
},
+6 -20
View File
@@ -17,7 +17,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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'),
});
},
});
+6 -22
View File
@@ -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'),
});
},
});
// -------------------------------------------
@@ -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'),
});
},
});
}
+59
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<void>}
*/
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;