Skip to content

Commit

Permalink
Merge branch 'main' into TP-202308-allowEdit
Browse files Browse the repository at this point in the history
  • Loading branch information
tpokorra authored Jan 7, 2025
2 parents e1786c0 + ab3bd5f commit 48f92a5
Show file tree
Hide file tree
Showing 11 changed files with 469 additions and 93 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@

# Changelog

## v5.0.0 - tbd

- **Unified Search integration**

You can now use the Unified Search to search forms based on the title and the description.

## v4.3.0 - 2024-10-04

- **New question type: Files**
Expand Down
11 changes: 11 additions & 0 deletions css/forms.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,14 @@
html {
scroll-padding-top: calc(var(--header-height) + 60px);
}

.icon-forms {
background-image: url(../img/forms-dark.svg);
filter: var(--background-invert-if-dark);
}

.icon-forms-white,
.icon-forms.icon-white {
background-image: url(../img/forms.svg);
filter: var(--background-invert-if-dark);
}
181 changes: 145 additions & 36 deletions l10n/sv.js

Large diffs are not rendered by default.

181 changes: 145 additions & 36 deletions l10n/sv.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use OCA\Forms\FormsMigrator;
use OCA\Forms\Listener\AnalyticsDatasourceListener;
use OCA\Forms\Listener\UserDeletedListener;
use OCA\Forms\Search\SearchProvider;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
Expand Down Expand Up @@ -42,6 +43,7 @@ public function register(IRegistrationContext $context): void {
$context->registerCapability(Capabilities::class);
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
$context->registerEventListener(DatasourceEvent::class, AnalyticsDatasourceListener::class);
$context->registerSearchProvider(SearchProvider::class);
$context->registerUserMigrator(FormsMigrator::class);
}

Expand Down
53 changes: 39 additions & 14 deletions lib/Db/FormMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,46 +97,47 @@ public function findByHash(string $hash): Form {
* @param string[] $groups IDs of groups the user is memeber of
* @param string[] $teams IDs of teams the user is memeber of
* @param bool $filterShown Set to false to also include forms shared but not visible on sidebar
* @param string $queryTerm optional: The search query for universal search
* @return Form[]
*/
public function findSharedForms(string $userId, array $groups = [], array $teams = [], bool $filterShown = true): array {
public function findSharedForms(string $userId, array $groups = [], array $teams = [], bool $filterShown = true, ?string $queryTerm = null): array {
$qbShares = $this->db->getQueryBuilder();
$qbForms = $this->db->getQueryBuilder();

$memberships = $qbShares->expr()->orX();
// share type user and share with current user
$memberships->add(
$qbShares->expr()->andX(
$qbShares->expr()->eq('shares.share_type', $qbShares->createNamedParameter(IShare::TYPE_USER)),
$qbShares->expr()->eq('shares.share_with', $qbShares->createNamedParameter($userId, IQueryBuilder::PARAM_STR)),
$qbShares->expr()->eq('shares.share_type', $qbShares->createNamedParameter(IShare::TYPE_USER, IQueryBuilder::PARAM_STR, ':share_type_user')),
$qbShares->expr()->eq('shares.share_with', $qbShares->createNamedParameter($userId, IQueryBuilder::PARAM_STR, ':share_with_user')),
),
);
// share type group and one of the user groups
if (!empty($groups)) {
$memberships->add(
$qbShares->expr()->andX(
$qbShares->expr()->eq('shares.share_type', $qbShares->createNamedParameter(IShare::TYPE_GROUP)),
$qbShares->expr()->in('shares.share_with', $qbShares->createNamedParameter($groups, IQueryBuilder::PARAM_STR_ARRAY)),
$qbShares->expr()->eq('shares.share_type', $qbShares->createNamedParameter(IShare::TYPE_GROUP, IQueryBuilder::PARAM_STR, ':share_type_group')),
$qbShares->expr()->in('shares.share_with', $qbShares->createNamedParameter($groups, IQueryBuilder::PARAM_STR_ARRAY, ':share_with_groups')),
),
);
}
// share type team and one of the user teams
if (!empty($teams)) {
$memberships->add(
$qbShares->expr()->andX(
$qbShares->expr()->eq('shares.share_type', $qbShares->createNamedParameter(IShare::TYPE_CIRCLE)),
$qbShares->expr()->in('shares.share_with', $qbShares->createNamedParameter($teams, IQueryBuilder::PARAM_STR_ARRAY)),
$qbShares->expr()->eq('shares.share_type', $qbShares->createNamedParameter(IShare::TYPE_CIRCLE, IQueryBuilder::PARAM_STR, ':share_type_team')),
$qbShares->expr()->in('shares.share_with', $qbShares->createNamedParameter($teams, IQueryBuilder::PARAM_STR_ARRAY, ':share_with_teams')),
),
);
}

// build expression for publicy shared forms (default only directly shown)
// build expression for publicly shared forms (default only directly shown)
if ($filterShown) {
// Only shown
$access = $qbShares->expr()->in('access_enum', $qbShares->createNamedParameter(Constants::FORM_ACCESS_ARRAY_SHOWN, IQueryBuilder::PARAM_INT_ARRAY));
$access = $qbShares->expr()->in('access_enum', $qbShares->createNamedParameter(Constants::FORM_ACCESS_ARRAY_SHOWN, IQueryBuilder::PARAM_INT_ARRAY, ':access_shown'));
} else {
// All
$access = $qbShares->expr()->neq('access_enum', $qbShares->createNamedParameter(Constants::FORM_ACCESS_NOPUBLICSHARE, IQueryBuilder::PARAM_INT));
$access = $qbShares->expr()->neq('access_enum', $qbShares->createNamedParameter(Constants::FORM_ACCESS_NOPUBLICSHARE, IQueryBuilder::PARAM_INT, ':access_nopublicshare'));
}

// Select all DISTINCT IDs of shared forms
Expand All @@ -145,7 +146,7 @@ public function findSharedForms(string $userId, array $groups = [], array $teams
->leftJoin('forms', $this->shareMapper->getTableName(), 'shares', $qbShares->expr()->eq('forms.id', 'shares.form_id'))
->where($memberships)
->orWhere($access)
->andWhere($qbShares->expr()->neq('forms.owner_id', $qbShares->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
->andWhere($qbShares->expr()->neq('forms.owner_id', $qbShares->createNamedParameter($userId, IQueryBuilder::PARAM_STR, ':owner_id')));

// Select the whole forms for the DISTINCT shared forms IDs
$qbForms->select('*')
Expand All @@ -156,16 +157,30 @@ public function findSharedForms(string $userId, array $groups = [], array $teams
->addOrderBy('last_updated', 'DESC')
->addOrderBy('created', 'DESC');

// We need to add the parameters from the shared forms IDs select to the final select query
$qbForms->setParameters($qbShares->getParameters(), $qbShares->getParameterTypes());
if ($queryTerm) {
$likeParameter = '%' . $this->db->escapeLikeParameter($queryTerm) . '%';
$qbForms->andWhere(
$qbForms->expr()->orX(
$qbForms->expr()->iLike('title', $qbForms->createNamedParameter($likeParameter, IQueryBuilder::PARAM_STR, ':query_term_title')),
$qbForms->expr()->iLike('description', $qbForms->createNamedParameter($likeParameter, IQueryBuilder::PARAM_STR, ':query_term_description'))
)
);
}

// Merge parameters and parameter types from $qbShares and $qbForms
$qbFormsParams = array_merge($qbShares->getParameters(), $qbForms->getParameters());
$qbFormsParamTypes = array_merge($qbShares->getParameterTypes(), $qbForms->getParameterTypes());

$qbForms->setParameters($qbFormsParams, $qbFormsParamTypes);

return $this->findEntities($qbForms);
}

/**
* @param string $queryTerm optional: The search query for universal search
* @return Form[]
*/
public function findAllByOwnerId(string $ownerId): array {
public function findAllByOwnerId(string $ownerId, ?string $queryTerm = null): array {
$qb = $this->db->getQueryBuilder();

$qb->select('*')
Expand All @@ -177,6 +192,16 @@ public function findAllByOwnerId(string $ownerId): array {
->addOrderBy('last_updated', 'DESC')
->addOrderBy('created', 'DESC');

if ($queryTerm) {
$likeParameter = '%' . $this->db->escapeLikeParameter($queryTerm) . '%';
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->iLike('title', $qb->createNamedParameter($likeParameter, IQueryBuilder::PARAM_STR, ':query_term_title')),
$qb->expr()->iLike('description', $qb->createNamedParameter($likeParameter, IQueryBuilder::PARAM_STR, ':query_term_description'))
)
);
}

return $this->findEntities($qb);
}

Expand Down
22 changes: 22 additions & 0 deletions lib/Search/FormsSearchResultEntry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Forms\Search;

use OCA\Forms\AppInfo\Application;
use OCA\Forms\Db\Form;
use OCP\IURLGenerator;
use OCP\Search\SearchResultEntry;

class FormsSearchResultEntry extends SearchResultEntry {
public function __construct(Form $form, IURLGenerator $urlGenerator) {
$formURL = $urlGenerator->linkToRoute('forms.page.views', ['hash' => $form->getHash(), 'view' => 'submit']);
$iconURL = $urlGenerator->getAbsoluteURL(($urlGenerator->imagePath(Application::APP_ID, 'forms-dark.svg')));
parent::__construct($iconURL, $form->getTitle(), $form->getDescription(), $formURL, 'icon-forms');
}
}
67 changes: 67 additions & 0 deletions lib/Search/SearchProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Forms\Search;

use OCA\Forms\AppInfo\Application;
use OCA\Forms\Db\Form;
use OCA\Forms\Service\FormsService;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\Search\IProvider;
use OCP\Search\ISearchQuery;
use OCP\Search\SearchResult;

class SearchProvider implements IProvider {
/**
* @psalm-suppress PossiblyUnusedMethod
*/
public function __construct(
private IL10N $l10n,
private IURLGenerator $urlGenerator,
private FormsService $formsService,
) {
}

public function getId(): string {
return 'forms';
}

public function getName(): string {
return $this->l10n->t('Forms');
}

public function search(IUser $user, ISearchQuery $query): SearchResult {
$forms = $this->formsService->search($query);

$results = array_map(function (Form $form) {
return [
'object' => $form,
'entry' => new FormsSearchResultEntry($form, $this->urlGenerator)
];
}, $forms);

$resultEntries = array_map(function (array $result) {
return $result['entry'];
}, $results);

return SearchResult::complete(
$this->l10n->t('Forms'),
$resultEntries
);
}

public function getOrder(string $route, array $routeParameters): int {
if (str_contains($route, Application::APP_ID)) {
// Active app, prefer my results
return -1;
}
return 77;
}
}
28 changes: 28 additions & 0 deletions lib/Service/FormsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Search\ISearchQuery;
use OCP\Security\ISecureRandom;
use OCP\Share\IShare;

Expand Down Expand Up @@ -743,6 +744,33 @@ public function areExtraSettingsValid(array $extraSettings, string $questionType
return true;
}

/**
* Get list of forms
*
* @param ISearchQuery $query the query to search the forms
* @return Form[] list of forms that match the query
*/
public function search(ISearchQuery $query): array {
$formsList = [];
$groups = $this->groupManager->getUserGroupIds($this->currentUser);
$teams = $this->circlesService->getUserTeamIds($this->currentUser->getUID());

try {
$ownedForms = $this->formMapper->findAllByOwnerId($this->currentUser->getUID(), $query->getTerm());
$sharedForms = $this->formMapper->findSharedForms(
$this->currentUser->getUID(),
$groups,
$teams,
true,
$query->getTerm()
);
$formsList = array_merge($ownedForms, $sharedForms);
} catch (DoesNotExistException $e) {
// silent catch
}
return $formsList;
}

public function getFilePath(Form $form): ?string {
$fileId = $form->getFileId();

Expand Down
6 changes: 2 additions & 4 deletions playwright/support/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { test as setup } from '@playwright/test'
import { configureNextcloud, docker } from '@nextcloud/cypress/docker'
import { configureNextcloud, getContainer } from '@nextcloud/cypress/docker'

/**
* We use this to ensure Nextcloud is configured correctly before running our tests
Expand All @@ -13,7 +13,5 @@ import { configureNextcloud, docker } from '@nextcloud/cypress/docker'
* as that only checks for the URL to be accessible which happens already before everything is configured.
*/
setup('Configure Nextcloud', async () => {
const containerName = 'nextcloud-cypress-tests_forms'
const container = docker.getContainer(containerName)
await configureNextcloud(['forms', 'viewer'], undefined, container)
await configureNextcloud(['forms', 'viewer'], undefined, getContainer())
})
5 changes: 2 additions & 3 deletions playwright/support/utils/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { docker } from '@nextcloud/cypress/docker'
import { getContainer } from '@nextcloud/cypress/docker'
import { expect, type APIRequestContext } from '@playwright/test'

/**
Expand All @@ -22,8 +22,7 @@ export async function runShell(
env?: Record<string, string | number>
},
) {
const containerName = 'nextcloud-cypress-tests_forms'
const container = docker.getContainer(containerName)
const container = getContainer()

const exec = await container.exec({
Cmd: ['sh', '-c', command],
Expand Down

0 comments on commit 48f92a5

Please sign in to comment.