Skip to content

Commit

Permalink
Import attachments on boot and on upload
Browse files Browse the repository at this point in the history
  • Loading branch information
adamziel committed Jan 1, 2025
1 parent 8594402 commit 6956b58
Show file tree
Hide file tree
Showing 15 changed files with 687 additions and 578 deletions.
469 changes: 268 additions & 201 deletions packages/playground/data-liberation-static-files-editor/plugin.php

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
useState,
createContext,
useContext,
createRoot,
} from '@wordpress/element';
import {
__experimentalTreeGrid as TreeGrid,
Expand Down Expand Up @@ -82,7 +83,7 @@ type DragState = {
path: string;
hoverPath: string | null;
hoverType: 'file' | 'folder' | null;
isExternal?: boolean;
isExternal: boolean;
};

type FilePickerContextType = {
Expand Down Expand Up @@ -113,6 +114,34 @@ type FilePickerContextType = {

const FilePickerContext = createContext<FilePickerContextType | null>(null);

function createDragImage(node: FileNode): HTMLElement {
const dragImage = ReactElementToHTML(<FileName node={node} />);
dragImage.style.cssText = `
position: fixed;
top: -1000px;
left: -1000px;
padding: 6px 12px;
background: white;
border-radius: 2px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
display: flex;
align-items: center;
justify-content: center;
font-family: -apple-system, system-ui, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
font-size: 13px;
white-space: nowrap;
`;
document.body.appendChild(dragImage);
return dragImage;
}

function ReactElementToHTML(element: React.ReactElement): HTMLElement {
const container = document.createElement('div');
const root = createRoot(container);
root.render(element);
return container;
}

export const FilePickerTree: React.FC<FilePickerControlProps> = ({
isLoading = false,
error = undefined,
Expand Down Expand Up @@ -256,13 +285,25 @@ export const FilePickerTree: React.FC<FilePickerControlProps> = ({
path: string,
type: 'file' | 'folder'
) => {
onDragStart(e, path, type);
e.stopPropagation();
const dragImage = createDragImage({
name: path.split('/').pop() || '',
type: type,
});
e.dataTransfer.setDragImage(dragImage, 10, 10);

// Clean up the drag image element after a short delay
setTimeout(() => {
document.body.removeChild(dragImage);
}, 0);

setDragState({
path,
hoverPath: null,
hoverType: null,
isExternal: false,
});

onDragStart?.(e, path, type);
};

const isDescendantPath = (parentPath: string, childPath: string) => {
Expand All @@ -281,9 +322,9 @@ export const FilePickerTree: React.FC<FilePickerControlProps> = ({
e.dataTransfer.dropEffect = 'copy';
setDragState({
path: '',
isExternal: true,
hoverPath: path,
hoverType: type,
isExternal: true,
});
return;
}
Expand Down Expand Up @@ -316,19 +357,51 @@ export const FilePickerTree: React.FC<FilePickerControlProps> = ({
return;
}
e.stopPropagation();
// Handle file/directory upload

// Internal drag&drop within the FilePickerTree
if (dragState && !dragState.isExternal) {
// Prevent dropping a folder into its own descendant
if (
dragState.path &&
targetType === 'folder' &&
isDescendantPath(dragState.path, targetPath)
) {
return;
}

const fromPath = dragState.path.replace(/^\/+/, '');

const targetParentPath =
targetType === 'file'
? targetPath.split('/').slice(0, -1).join('/')
: targetPath;

const toPath = [targetParentPath, dragState.path.split('/').pop()]
.join('/')
.replace(/^\/+/, '');

setDragState(null);

if (fromPath === toPath) {
return;
}

onNodeMoved({
fromPath,
toPath,
});
return;
}

// Drag&Drop from desktop into the FilePickerTree
if (e.dataTransfer.items.length > 0) {
const targetFolder =
targetType === 'folder'
? targetPath
: targetPath.split('/').slice(0, -1).join('/');
const items = Array.from(e.dataTransfer.items).filter(
(item) => item.kind !== 'DownloadURL'
);

const items = Array.from(e.dataTransfer.items);
const buildTree = async (
entry: FileSystemEntry,
parentPath: string = ''
entry: FileSystemEntry
): Promise<FileNode> => {
if (entry.isFile) {
const fileEntry = entry as FileSystemFileEntry;
Expand Down Expand Up @@ -376,39 +449,6 @@ export const FilePickerTree: React.FC<FilePickerControlProps> = ({
setDragState(null);
return;
}

if (dragState) {
// Prevent dropping a folder into its own descendant
if (
dragState.path &&
targetType === 'folder' &&
isDescendantPath(dragState.path, targetPath)
) {
return;
}

const fromPath = dragState.path.replace(/^\/+/, '');

const targetParentPath =
targetType === 'file'
? targetPath.split('/').slice(0, -1).join('/')
: targetPath;

const toPath = [targetParentPath, dragState.path.split('/').pop()]
.join('/')
.replace(/^\/+/, '');

setDragState(null);

if (fromPath === toPath) {
return;
}

onNodeMoved({
fromPath,
toPath,
});
}
};

const handleDragEnd = () => {
Expand Down Expand Up @@ -740,7 +780,7 @@ const NodeRow: React.FC<{
position: 'relative',
}}
>
<FileName
<FileButtonContent
node={node}
isOpen={
node.type === 'folder' &&
Expand Down Expand Up @@ -873,11 +913,11 @@ const FilenameForm: React.FC<{
);
};

const FileName: React.FC<{
const FileButtonContent: React.FC<{
node: FileNode;
level: number;
isOpen?: boolean;
}> = ({ node, level, isOpen }) => {
}> = ({ node, level, isOpen = false }) => {
const indent: string[] = [];
for (let i = 0; i < level; i++) {
indent.push('&nbsp;&nbsp;&nbsp;&nbsp;');
Expand All @@ -893,6 +933,16 @@ const FileName: React.FC<{
) : (
<div style={{ width: 16 }}>&nbsp;</div>
)}
<FileName node={node} />
</>
);
};

const FileName: React.FC<{
node: FileNode;
}> = ({ node }) => {
return (
<>
<Icon width={16} icon={node.type === 'folder' ? folder : file} />
<span className={css['fileName']}>{node.name}</span>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,15 @@ const uiStore = createReduxStore(STORE_NAME, {

register(uiStore);

const isStaticPagePath = (path: string) => {
const extension = path.split('.').pop()?.toLowerCase();
return ['md', 'html'].includes(extension);
const isStaticAssetPath = (path: string) => {
let extension = undefined;
const lastDot = path.lastIndexOf('.');
if (lastDot !== -1) {
extension = path.substring(lastDot + 1).toLowerCase();
}
// We treat every extension except of the well-known ones
// as a static asset.
return extension && !['md', 'html', 'xhtml'].includes(extension);
};

function ConnectedFilePickerTree() {
Expand All @@ -79,7 +85,16 @@ function ConnectedFilePickerTree() {

useEffect(() => {
async function refreshPostId() {
if (isStaticPagePath(selectedPath)) {
console.log(
'refreshPostId',
selectedPath,
isStaticAssetPath(selectedPath)
);
if (isStaticAssetPath(selectedPath)) {
setPostLoading(false);
setSelectedPostId(null);
setPreviewPath(selectedPath);
} else {
setPostLoading(true);
if (!selectedPostId) {
const { post_id } = (await apiFetch({
Expand All @@ -90,10 +105,6 @@ function ConnectedFilePickerTree() {
setSelectedPostId(post_id);
}
setPreviewPath(null);
} else {
setPostLoading(false);
setSelectedPostId(null);
setPreviewPath(selectedPath);
}
}
refreshPostId();
Expand Down Expand Up @@ -180,7 +191,7 @@ function ConnectedFilePickerTree() {

const handleFileClick = async (filePath: string, node: FileNode) => {
setSelectedPath(filePath);
if (isStaticPagePath(filePath)) {
if (node.post_id && !isStaticAssetPath(filePath)) {
setSelectedPostId(node.post_id);
} else {
setSelectedPostId(null);
Expand Down Expand Up @@ -220,7 +231,10 @@ function ConnectedFilePickerTree() {

await refreshFileTree();

if (response.created_files.length > 0) {
if (
response.created_files.length === 1 &&
response.created_files[0].post_type === WP_LOCAL_FILE_POST_TYPE
) {
onNavigateToEntityRecord({
postId: response.created_files[0].post_id,
postType: WP_LOCAL_FILE_POST_TYPE,
Expand Down Expand Up @@ -266,10 +280,16 @@ function ConnectedFilePickerTree() {
if (type === 'file') {
const url = `${window.wpApiSettings.root}static-files-editor/v1/download-file?path=${path}&_wpnonce=${window.wpApiSettings.nonce}`;
const filename = path.split('/').pop();
// For dragging & dropping to desktop
e.dataTransfer.setData(
'DownloadURL',
`text/plain:${filename}:${url}`
);
// For dragging & dropping into the editor canvas
e.dataTransfer.setData(
'text/html',
`<img src="${url}" alt="${filename}" />`
);
}
};

Expand Down
2 changes: 2 additions & 0 deletions packages/playground/data-liberation/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_SQLite_Filesystem.php';
require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_File_Visitor_Event.php';
require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Filesystem_Visitor.php';
require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Filesystem_Chroot.php';
require_once __DIR__ . '/blueprints-library/src/WordPress/Filesystem/WP_Uploaded_Directory_Tree_Filesystem.php';

require_once __DIR__ . '/blueprints-library/src/WordPress/ByteReader/WP_Byte_Reader.php';
require_once __DIR__ . '/blueprints-library/src/WordPress/ByteReader/WP_File_Reader.php';
Expand Down
18 changes: 11 additions & 7 deletions packages/playground/data-liberation/plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ function data_liberation_import_step( $session, $importer = null ) {
$importer = data_liberation_create_importer( $metadata );
}
if ( ! $importer ) {
return;
return new WP_Error('import_failed', 'Failed to create importer');
}

/**
Expand All @@ -372,33 +372,35 @@ function data_liberation_import_step( $session, $importer = null ) {
// frontloading stage.
if ( $importer->get_stage() === WP_Stream_Importer::STAGE_FRONTLOAD_ASSETS ) {
if ( $fetched_files > 0 ) {
break;
return new WP_Error('import_failed', 'Frontloading failed');
}
} else {
break;
return new WP_Error('import_failed', 'Frontloading failed');
}
}
if ( $time_taken >= $hard_time_limit_seconds ) {
// No negotiation, we're done.
// @TODO: Make it easily configurable
// @TODO: Bump the number of download attempts for the placeholders,
// set the status to `error` in each interrupted download.
break;
return new WP_Error('import_failed', 'Time limit exceeded');
}

if ( true !== $importer->next_step() ) {
if ( ! $importer->next_step() ) {
// Time to advance to the next stage.
$session->set_reentrancy_cursor( $importer->get_reentrancy_cursor() );

$should_advance_to_next_stage = null !== $importer->get_next_stage();
if ( $should_advance_to_next_stage ) {
if ( WP_Stream_Importer::STAGE_FRONTLOAD_ASSETS === $importer->get_stage() ) {
$resolved_all_failures = $session->count_unfinished_frontloading_placeholders() === 0;
if ( ! $resolved_all_failures ) {
break;
return new WP_Error('import_failed', 'Downloads failed');
}
}
}
if ( ! $importer->advance_to_next_stage() ) {
// We're done.
break;
}
$session->set_stage( $importer->get_stage() );
Expand All @@ -409,7 +411,9 @@ function data_liberation_import_step( $session, $importer = null ) {
switch ( $importer->get_stage() ) {
case WP_Stream_Importer::STAGE_INDEX_ENTITIES:
// Bump the total number of entities to import.
$session->create_frontloading_placeholders( $importer->get_indexed_assets_urls() );
$session->create_frontloading_placeholders(
$importer->get_indexed_assets_urls()
);
$session->bump_total_number_of_entities(
$importer->get_indexed_entities_counts()
);
Expand Down
Loading

0 comments on commit 6956b58

Please sign in to comment.