Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Navigate via folder tree #46596

Merged
merged 11 commits into from
Aug 1, 2024
5 changes: 5 additions & 0 deletions apps/files/appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@
'url' => '/api/v1/views/{view}/{key}',
'verb' => 'PUT'
],
[
'name' => 'Api#setViewConfig',
'url' => '/api/v1/views',
'verb' => 'PUT'
],
[
'name' => 'Api#getViewConfigs',
'url' => '/api/v1/views',
Expand Down
6 changes: 6 additions & 0 deletions apps/files/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@
use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
use OCP\Files\Events\Node\NodeCopiedEvent;
use OCP\Files\IRootFolder;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IPreview;
use OCP\IRequest;
use OCP\IServerContainer;
Expand All @@ -53,6 +55,7 @@
use OCP\Share\IManager as IShareManager;
use OCP\Util;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;

class Application extends App implements IBootstrap {
public const APP_ID = 'files';
Expand Down Expand Up @@ -80,6 +83,9 @@ public function register(IRegistrationContext $context): void {
$server->getUserFolder(),
$c->get(UserConfig::class),
$c->get(ViewConfig::class),
$c->get(IL10N::class),
$c->get(IRootFolder::class),
$c->get(LoggerInterface::class),
);
});

Expand Down
122 changes: 97 additions & 25 deletions apps/files/lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@
*/
namespace OCA\Files\Controller;

use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException;
use OC\Files\Node\Node;
use OC\Files\Search\SearchComparison;
use OC\Files\Search\SearchQuery;
use OCA\Files\ResponseDefinitions;
use OCA\Files\Service\TagService;
use OCA\Files\Service\UserConfig;
use OCA\Files\Service\ViewConfig;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\ApiRoute;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\OpenAPI;
Expand All @@ -24,48 +29,44 @@
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\StreamResponse;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\Search\ISearchComparison;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IPreview;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Share\IManager;
use OCP\Share\IShare;
use Psr\Log\LoggerInterface;
use Throwable;

/**
* @psalm-import-type FilesFolderTree from ResponseDefinitions
*
* @package OCA\Files\Controller
*/
class ApiController extends Controller {
private TagService $tagService;
private IManager $shareManager;
private IPreview $previewManager;
private IUserSession $userSession;
private IConfig $config;
private ?Folder $userFolder;
private UserConfig $userConfig;
private ViewConfig $viewConfig;

public function __construct(string $appName,
IRequest $request,
IUserSession $userSession,
TagService $tagService,
IPreview $previewManager,
IManager $shareManager,
IConfig $config,
?Folder $userFolder,
UserConfig $userConfig,
ViewConfig $viewConfig) {
private IUserSession $userSession,
private TagService $tagService,
private IPreview $previewManager,
private IManager $shareManager,
private IConfig $config,
private ?Folder $userFolder,
private UserConfig $userConfig,
private ViewConfig $viewConfig,
private IL10N $l10n,
private IRootFolder $rootFolder,
private LoggerInterface $logger,
) {
parent::__construct($appName, $request);
$this->userSession = $userSession;
$this->tagService = $tagService;
$this->previewManager = $previewManager;
$this->shareManager = $shareManager;
$this->config = $config;
$this->userFolder = $userFolder;
$this->userConfig = $userConfig;
$this->viewConfig = $viewConfig;
}

/**
Expand Down Expand Up @@ -232,6 +233,77 @@ public function getRecentFiles() {
return new DataResponse(['files' => $files]);
}

/**
* @param Folder[] $folders
*/
private function getTree(array $folders): array {
$user = $this->userSession->getUser();
if (!($user instanceof IUser)) {
throw new NotLoggedInException();
}

$userFolder = $this->rootFolder->getUserFolder($user->getUID());
Fixed Show fixed Hide fixed
$tree = [];
foreach ($folders as $folder) {
$path = $userFolder->getRelativePath($folder->getPath());
if ($path === null) {
continue;
}
$pathBasenames = explode('/', trim($path, '/'));
$current = &$tree;
foreach ($pathBasenames as $basename) {
if (!isset($current['children'][$basename])) {
$current['children'][$basename] = [
'id' => $folder->getId(),
];
$displayName = $folder->getName();
if ($displayName !== $basename) {
$current['children'][$basename]['displayName'] = $displayName;
}
}
$current = &$current['children'][$basename];
}
}
return $tree['children'] ?? $tree;
}

/**
* Returns the folder tree of the user
*
* @return JSONResponse<Http::STATUS_OK, FilesFolderTree, array{}>|JSONResponse<Http::STATUS_UNAUTHORIZED, array{message: string}, array{}>
Fixed Show fixed Hide fixed
*
* 200: Folder tree returned successfully
* 401: Unauthorized
*/
#[NoAdminRequired]
#[ApiRoute(verb: 'GET', url: '/api/v1/folder-tree')]
public function getFolderTree(): JSONResponse {
$user = $this->userSession->getUser();
if (!($user instanceof IUser)) {
return new JSONResponse([
'message' => $this->l10n->t('Failed to authorize'),
], Http::STATUS_UNAUTHORIZED);
}

$userFolder = $this->rootFolder->getUserFolder($user->getUID());
Fixed Show fixed Hide fixed
try {
$searchQuery = new SearchQuery(
new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE),
0,
0,
[],
$user,
false,
);
/** @var Folder[] $folders */
$folders = $userFolder->search($searchQuery);
$tree = $this->getTree($folders);
} catch (Throwable $th) {
$this->logger->error($th->getMessage(), ['exception' => $th]);
$tree = [];
}
return new JSONResponse($tree, Http::STATUS_OK, [], JSON_FORCE_OBJECT);
}

/**
* Returns the current logged-in user's storage stats.
Expand Down
9 changes: 9 additions & 0 deletions apps/files/lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@
* content: string,
* type: string,
* }
*
* @psalm-type FilesFolderTreeNode = array{
* id: int,
* displayName?: string,
* children?: array<string, array{}>,
* }
*
* @psalm-type FilesFolderTree = array<string, FilesFolderTreeNode>
*
*/
class ResponseDefinitions {
}
8 changes: 7 additions & 1 deletion apps/files/lib/Service/UserConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ class UserConfig {
'default' => false,
'allowed' => [true, false],
],
[
// Whether to show the folder tree
'key' => 'folder_tree',
'default' => true,
'allowed' => [true, false],
],
];

protected IConfig $config;
Expand Down Expand Up @@ -108,7 +114,7 @@ public function setConfig(string $key, $value): void {
if (!in_array($key, $this->getAllowedConfigKeys())) {
throw new \InvalidArgumentException('Unknown config key');
}

if (!in_array($value, $this->getAllowedConfigValues($key))) {
throw new \InvalidArgumentException('Invalid config value');
}
Expand Down
86 changes: 86 additions & 0 deletions apps/files/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,33 @@
}
}
},
"FolderTree": {
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/FolderTreeNode"
}
},
"FolderTreeNode": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"displayName": {
"type": "string"
},
"children": {
"type": "object",
"additionalProperties": {
"type": "object"
}
}
}
},
"OCSMeta": {
"type": "object",
"required": [
Expand Down Expand Up @@ -1928,6 +1955,65 @@
}
}
}
},
"/ocs/v2.php/apps/files/api/v1/folder-tree": {
"get": {
"operationId": "api-get-folder-tree",
"summary": "Returns the folder tree of the user",
"tags": [
"api"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Folder tree returned successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FolderTree"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
}
}
}
}
}
}
}
}
}
},
"tags": []
Expand Down
24 changes: 22 additions & 2 deletions apps/files/src/components/BreadCrumbs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,11 @@ export default defineComponent({
return this.dirs.map((dir: string, index: number) => {
const source = this.getFileSourceFromPath(dir)
const node: Node | undefined = source ? this.getNodeFromSource(source) : undefined
const to = { ...this.$route, params: { node: node?.fileid }, query: { dir } }
return {
dir,
exact: true,
name: this.getDirDisplayName(dir),
to,
to: this.getTo(dir, node),
// disable drop on current directory
disableDrop: index === this.dirs.length - 1,
}
Expand Down Expand Up @@ -163,6 +162,27 @@ export default defineComponent({
return node?.displayname || basename(path)
},

getTo(dir: string, node?: Node): Record<string, unknown> {
if (dir === '/') {
return {
...this.$route,
params: { view: this.currentView?.id },
query: {},
}
}
if (node === undefined) {
return {
...this.$route,
query: { dir },
}
}
return {
...this.$route,
params: { fileid: String(node.fileid) },
query: { dir: node.path },
}
},

onClick(to) {
if (to?.query?.dir === this.$route.query.dir) {
this.$emit('reload')
Expand Down
Loading
Loading