mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-22 21:40:55 +01:00
221562b294
Summary: Modernizes file uploads. In particular: - Adds a mobile menu, with an "Upload File" item. - Adds crumbs to the list view, detail view and upload view. - Adds "Upload File" action to crumbs. - Moves upload file to a separate page. - Removes the combined upload file + recent files page. - Makes upload file use a normal file control by default (works on mobile). - Home page, file list and file upload page are now global drop targets which accept files dropped anywhere on them. Dragging a file into the window shows a mask and an instructional message. - User education on this is a little weak but I think that's a big can of worms? - Fixes a bug where dropping multiple files into a Remarkup text area produced bad results (resolves T2190). T879 is related, although it's specifically about Maniphest. I've declined to make global drop targets yet there because there are multiple drop targets on the page with different meanings. That UI needs updating in general. @chad, do we have an "upload" icon (counterpart to "download")? Test Plan: Uploaded files in Maniphest, Differential, Files, and from Home. Dragged and dropped multiple files into Differential. Used crumbs, mobile. Reviewers: chad, btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T2190 Differential Revision: https://secure.phabricator.com/D4200
405 lines
12 KiB
PHP
Executable file
405 lines
12 KiB
PHP
Executable file
#!/usr/bin/env php
|
|
<?php
|
|
|
|
$package_spec = array(
|
|
'javelin.pkg.js' => array(
|
|
'javelin-util',
|
|
'javelin-install',
|
|
'javelin-event',
|
|
'javelin-stratcom',
|
|
'javelin-behavior',
|
|
'javelin-resource',
|
|
'javelin-request',
|
|
'javelin-vector',
|
|
'javelin-dom',
|
|
'javelin-json',
|
|
'javelin-uri',
|
|
'javelin-workflow',
|
|
'javelin-mask',
|
|
'javelin-typeahead',
|
|
'javelin-typeahead-normalizer',
|
|
'javelin-typeahead-source',
|
|
'javelin-typeahead-preloaded-source',
|
|
'javelin-typeahead-ondemand-source',
|
|
'javelin-tokenizer',
|
|
),
|
|
'core.pkg.js' => array(
|
|
'javelin-behavior-aphront-basic-tokenizer',
|
|
'javelin-behavior-workflow',
|
|
'javelin-behavior-aphront-form-disable-on-submit',
|
|
'phabricator-keyboard-shortcut-manager',
|
|
'phabricator-keyboard-shortcut',
|
|
'javelin-behavior-phabricator-keyboard-shortcuts',
|
|
'javelin-behavior-refresh-csrf',
|
|
'javelin-behavior-phabricator-watch-anchor',
|
|
'javelin-behavior-phabricator-autofocus',
|
|
'phabricator-paste-file-upload',
|
|
'phabricator-menu-item',
|
|
'phabricator-dropdown-menu',
|
|
'javelin-behavior-phabricator-oncopy',
|
|
'phabricator-tooltip',
|
|
'javelin-behavior-phabricator-tooltips',
|
|
'phabricator-prefab',
|
|
'javelin-behavior-device',
|
|
'javelin-behavior-toggle-class',
|
|
'javelin-behavior-lightbox-attachments',
|
|
'phabricator-busy',
|
|
'javelin-aphlict',
|
|
'phabricator-notification',
|
|
'javelin-behavior-aphlict-listen',
|
|
'javelin-behavior-phabricator-search-typeahead',
|
|
'javelin-behavior-konami',
|
|
'javelin-behavior-aphlict-dropdown',
|
|
|
|
'javelin-behavior-phabricator-active-nav',
|
|
'javelin-behavior-phabricator-nav',
|
|
'javelin-behavior-phabricator-remarkup-assist',
|
|
'phabricator-textareautils',
|
|
'phabricator-file-upload',
|
|
'javelin-behavior-global-drag-and-drop',
|
|
),
|
|
'core.pkg.css' => array(
|
|
'phabricator-core-css',
|
|
'phabricator-core-buttons-css',
|
|
'phabricator-standard-page-view',
|
|
'aphront-dialog-view-css',
|
|
'aphront-form-view-css',
|
|
'aphront-panel-view-css',
|
|
'aphront-side-nav-view-css',
|
|
'aphront-table-view-css',
|
|
'aphront-crumbs-view-css',
|
|
'aphront-tokenizer-control-css',
|
|
'aphront-typeahead-control-css',
|
|
'aphront-list-filter-view-css',
|
|
|
|
'phabricator-directory-css',
|
|
'phabricator-jump-nav',
|
|
'phabricator-app-buttons-css',
|
|
|
|
'phabricator-remarkup-css',
|
|
'syntax-highlighting-css',
|
|
'aphront-pager-view-css',
|
|
'phabricator-transaction-view-css',
|
|
'aphront-tooltip-css',
|
|
'aphront-headsup-view-css',
|
|
'phabricator-flag-css',
|
|
'aphront-error-view-css',
|
|
|
|
'sprite-icon-css',
|
|
'sprite-gradient-css',
|
|
'sprite-menu-css',
|
|
'sprite-apps-large-css',
|
|
|
|
'phabricator-main-menu-view',
|
|
'phabricator-notification-css',
|
|
'phabricator-notification-menu-css',
|
|
'lightbox-attachment-css',
|
|
'phabricator-header-view-css',
|
|
'phabricator-form-view-css',
|
|
'phabricator-filetree-view-css',
|
|
'phabricator-nav-view-css',
|
|
'phabricator-side-menu-view-css',
|
|
'phabricator-crumbs-view-css',
|
|
'phabricator-object-item-list-view-css',
|
|
'global-drag-and-drop-css',
|
|
|
|
),
|
|
'differential.pkg.css' => array(
|
|
'differential-core-view-css',
|
|
'differential-changeset-view-css',
|
|
'differential-results-table-css',
|
|
'differential-revision-history-css',
|
|
'differential-revision-list-css',
|
|
'differential-table-of-contents-css',
|
|
'differential-revision-comment-css',
|
|
'differential-revision-add-comment-css',
|
|
'differential-revision-comment-list-css',
|
|
'phabricator-object-selector-css',
|
|
'aphront-headsup-action-list-view-css',
|
|
'phabricator-content-source-view-css',
|
|
'differential-local-commits-view-css',
|
|
'inline-comment-summary-css',
|
|
),
|
|
'differential.pkg.js' => array(
|
|
'phabricator-drag-and-drop-file-upload',
|
|
'phabricator-shaped-request',
|
|
|
|
'javelin-behavior-differential-feedback-preview',
|
|
'javelin-behavior-differential-edit-inline-comments',
|
|
'javelin-behavior-differential-populate',
|
|
'javelin-behavior-differential-show-more',
|
|
'javelin-behavior-differential-diff-radios',
|
|
'javelin-behavior-differential-accept-with-errors',
|
|
'javelin-behavior-differential-comment-jump',
|
|
'javelin-behavior-differential-add-reviewers-and-ccs',
|
|
'javelin-behavior-differential-keyboard-navigation',
|
|
'javelin-behavior-aphront-drag-and-drop',
|
|
'javelin-behavior-aphront-drag-and-drop-textarea',
|
|
'javelin-behavior-phabricator-object-selector',
|
|
'javelin-behavior-repository-crossreference',
|
|
|
|
'differential-inline-comment-editor',
|
|
'javelin-behavior-differential-dropdown-menus',
|
|
'javelin-behavior-differential-toggle-files',
|
|
'javelin-behavior-differential-user-select',
|
|
),
|
|
'diffusion.pkg.css' => array(
|
|
'diffusion-commit-view-css',
|
|
'diffusion-icons-css',
|
|
),
|
|
'diffusion.pkg.js' => array(
|
|
'javelin-behavior-diffusion-pull-lastmodified',
|
|
'javelin-behavior-diffusion-commit-graph',
|
|
'javelin-behavior-audit-preview',
|
|
),
|
|
'maniphest.pkg.css' => array(
|
|
'maniphest-task-summary-css',
|
|
'maniphest-transaction-detail-css',
|
|
'aphront-attached-file-view-css',
|
|
'phabricator-project-tag-css',
|
|
),
|
|
'maniphest.pkg.js' => array(
|
|
'javelin-behavior-maniphest-batch-selector',
|
|
'javelin-behavior-maniphest-transaction-controls',
|
|
'javelin-behavior-maniphest-transaction-preview',
|
|
'javelin-behavior-maniphest-transaction-expand',
|
|
'javelin-behavior-maniphest-subpriority-editor',
|
|
),
|
|
'darkconsole.pkg.js' => array(
|
|
'javelin-behavior-dark-console-ajax',
|
|
'javelin-behavior-dark-console',
|
|
'javelin-behavior-error-log',
|
|
),
|
|
);
|
|
|
|
|
|
require_once dirname(__FILE__).'/__init_script__.php';
|
|
|
|
$args = new PhutilArgumentParser($argv);
|
|
$args->setTagline('map static resources');
|
|
$args->setSynopsis(
|
|
"**celerity_mapper.php** [--output __path__] [--with-custom] <webroot>");
|
|
$args->parse(
|
|
array(
|
|
array(
|
|
'name' => 'output',
|
|
'param' => 'path',
|
|
'default' => '../src/__celerity_resource_map__.php',
|
|
'help' => "Set the path for resource map. It is usually useful for ".
|
|
"'celerity.resource-path' configuration.",
|
|
),
|
|
array(
|
|
'name' => 'with-custom',
|
|
'help' => 'Include resources in <webroot>/rsrc/custom/.',
|
|
),
|
|
array(
|
|
'name' => 'webroot',
|
|
'wildcard' => true,
|
|
),
|
|
));
|
|
|
|
$root = $args->getArg('webroot');
|
|
if (count($root) != 1 || !is_dir(reset($root))) {
|
|
$args->printHelpAndExit();
|
|
}
|
|
$root = Filesystem::resolvePath(reset($root));
|
|
|
|
$celerity_path = Filesystem::resolvePath($args->getArg('output'), $root);
|
|
$with_custom = $args->getArg('with-custom');
|
|
|
|
$resource_hash = PhabricatorEnv::getEnvConfig('celerity.resource-hash');
|
|
$runtime_map = array();
|
|
|
|
echo "Finding raw static resources...\n";
|
|
$finder = id(new FileFinder($root))
|
|
->withType('f')
|
|
->withSuffix('png')
|
|
->withSuffix('jpg')
|
|
->withSuffix('gif')
|
|
->withSuffix('swf')
|
|
->withFollowSymlinks(true)
|
|
->setGenerateChecksums(true);
|
|
if (!$with_custom) {
|
|
$finder->excludePath('./rsrc/custom');
|
|
}
|
|
$raw_files = $finder->find();
|
|
|
|
echo "Processing ".count($raw_files)." files";
|
|
foreach ($raw_files as $path => $hash) {
|
|
echo ".";
|
|
$path = '/'.Filesystem::readablePath($path, $root);
|
|
$type = CelerityResourceTransformer::getResourceType($path);
|
|
|
|
$hash = md5($hash.$path.$resource_hash);
|
|
$uri = '/res/'.substr($hash, 0, 8).$path;
|
|
|
|
$runtime_map[$path] = array(
|
|
'hash' => $hash,
|
|
'uri' => $uri,
|
|
'disk' => $path,
|
|
'type' => $type,
|
|
);
|
|
}
|
|
echo "\n";
|
|
|
|
$xformer = id(new CelerityResourceTransformer())
|
|
->setMinify(false)
|
|
->setRawResourceMap($runtime_map);
|
|
|
|
echo "Finding transformable static resources...\n";
|
|
$finder = id(new FileFinder($root))
|
|
->withType('f')
|
|
->withSuffix('js')
|
|
->withSuffix('css')
|
|
->withFollowSymlinks(true)
|
|
->setGenerateChecksums(true);
|
|
if (!$with_custom) {
|
|
$finder->excludePath('./rsrc/custom');
|
|
}
|
|
$files = $finder->find();
|
|
|
|
echo "Processing ".count($files)." files";
|
|
|
|
$file_map = array();
|
|
foreach ($files as $path => $raw_hash) {
|
|
echo ".";
|
|
$path = '/'.Filesystem::readablePath($path, $root);
|
|
$data = Filesystem::readFile($root.$path);
|
|
|
|
$data = $xformer->transformResource($path, $data);
|
|
$hash = md5($data);
|
|
$hash = md5($hash.$path.$resource_hash);
|
|
|
|
$file_map[$path] = array(
|
|
'hash' => $hash,
|
|
'disk' => $path,
|
|
);
|
|
}
|
|
echo "\n";
|
|
|
|
$resource_graph = array();
|
|
$hash_map = array();
|
|
|
|
$parser = new PhutilDocblockParser();
|
|
foreach ($file_map as $path => $info) {
|
|
$type = CelerityResourceTransformer::getResourceType($path);
|
|
|
|
$data = Filesystem::readFile($root.$info['disk']);
|
|
$matches = array();
|
|
$ok = preg_match('@/[*][*].*?[*]/@s', $data, $matches);
|
|
if (!$ok) {
|
|
throw new Exception(
|
|
"File {$path} does not have a header doc comment. Encode dependency ".
|
|
"data in a header docblock.");
|
|
}
|
|
|
|
list($description, $metadata) = $parser->parse($matches[0]);
|
|
|
|
$provides = preg_split('/\s+/', trim(idx($metadata, 'provides')));
|
|
$requires = preg_split('/\s+/', trim(idx($metadata, 'requires')));
|
|
$provides = array_filter($provides);
|
|
$requires = array_filter($requires);
|
|
|
|
if (!$provides) {
|
|
// Tests and documentation-only JS is permitted to @provide no targets.
|
|
continue;
|
|
}
|
|
|
|
if (count($provides) > 1) {
|
|
throw new Exception(
|
|
"File {$path} must @provide at most one Celerity target.");
|
|
}
|
|
|
|
$provides = reset($provides);
|
|
|
|
$uri = '/res/'.substr($info['hash'], 0, 8).$path;
|
|
|
|
$hash_map[$provides] = $info['hash'];
|
|
|
|
$resource_graph[$provides] = $requires;
|
|
|
|
$runtime_map[$provides] = array(
|
|
'uri' => $uri,
|
|
'type' => $type,
|
|
'requires' => $requires,
|
|
'disk' => $path,
|
|
);
|
|
}
|
|
|
|
$celerity_resource_graph = new CelerityResourceGraph();
|
|
$celerity_resource_graph->addNodes($resource_graph);
|
|
$celerity_resource_graph->setResourceGraph($resource_graph);
|
|
$celerity_resource_graph->loadGraph();
|
|
|
|
foreach ($resource_graph as $provides => $requires) {
|
|
$cycle = $celerity_resource_graph->detectCycles($provides);
|
|
if ($cycle) {
|
|
throw new Exception(
|
|
"Cycle detected in resource graph: ". implode($cycle, " => ")
|
|
);
|
|
}
|
|
}
|
|
|
|
$package_map = array();
|
|
foreach ($package_spec as $name => $package) {
|
|
$hashes = array();
|
|
$type = null;
|
|
foreach ($package as $symbol) {
|
|
if (empty($hash_map[$symbol])) {
|
|
throw new Exception(
|
|
"Package specification for '{$name}' includes '{$symbol}', but that ".
|
|
"symbol is not defined anywhere.");
|
|
}
|
|
if ($type === null) {
|
|
$type = $runtime_map[$symbol]['type'];
|
|
} else {
|
|
$ntype = $runtime_map[$symbol]['type'];
|
|
if ($type !== $ntype) {
|
|
throw new Exception(
|
|
"Package specification for '{$name}' mixes resources of type ".
|
|
"'{$type}' with resources of type '{$ntype}'. Each package may only ".
|
|
"contain one type of resource.");
|
|
}
|
|
}
|
|
$hashes[] = $symbol.':'.$hash_map[$symbol];
|
|
}
|
|
$key = substr(md5(implode("\n", $hashes)), 0, 8);
|
|
$package_map['packages'][$key] = array(
|
|
'name' => $name,
|
|
'symbols' => $package,
|
|
'uri' => '/res/pkg/'.$key.'/'.$name,
|
|
'type' => $type,
|
|
);
|
|
foreach ($package as $symbol) {
|
|
$package_map['reverse'][$symbol] = $key;
|
|
}
|
|
}
|
|
|
|
ksort($runtime_map);
|
|
$runtime_map = var_export($runtime_map, true);
|
|
$runtime_map = preg_replace('/\s+$/m', '', $runtime_map);
|
|
$runtime_map = preg_replace('/array \(/', 'array(', $runtime_map);
|
|
|
|
$package_map['packages'] = isort($package_map['packages'], 'name');
|
|
ksort($package_map['reverse']);
|
|
$package_map = var_export($package_map, true);
|
|
$package_map = preg_replace('/\s+$/m', '', $package_map);
|
|
$package_map = preg_replace('/array \(/', 'array(', $package_map);
|
|
|
|
$generated = '@'.'generated';
|
|
$resource_map = <<<EOFILE
|
|
<?php
|
|
|
|
/**
|
|
* This file is automatically generated. Use 'celerity_mapper.php' to rebuild
|
|
* it.
|
|
* {$generated}
|
|
*/
|
|
|
|
celerity_register_resource_map({$runtime_map}, {$package_map});
|
|
|
|
EOFILE;
|
|
|
|
echo "Writing map...\n";
|
|
Filesystem::writeFile($celerity_path, $resource_map);
|
|
echo "Done.\n";
|