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

[OP#48106] use same component as project tab for searching in smart picker #448

Merged
merged 5 commits into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions lib/Controller/OpenProjectAPIController.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,17 +154,18 @@ public function getNotifications(): DataResponse {
*
* @return DataResponse
*/
public function getSearchedWorkPackages(?string $searchQuery = null, ?int $fileId = null): DataResponse {
public function getSearchedWorkPackages(?string $searchQuery = null, ?int $fileId = null, bool $isSmartPicker = false): DataResponse {
if ($this->accessToken === '') {
return new DataResponse('', Http::STATUS_UNAUTHORIZED);
} elseif (!OpenProjectAPIService::validateURL($this->openprojectUrl)) {
return new DataResponse('', Http::STATUS_BAD_REQUEST);
}

// when the search is done through smart picker we don't want to check if the work package is linkable
$result = $this->openprojectAPIService->searchWorkPackage(
$this->userId,
$searchQuery,
$fileId
$fileId,
!$isSmartPicker
);

if (!isset($result['error'])) {
Expand Down
27 changes: 27 additions & 0 deletions lib/Listener/OpenProjectReferenceListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,48 @@
namespace OCA\OpenProject\Listener;

use OCA\OpenProject\AppInfo\Application;
use OCA\OpenProject\Service\OpenProjectAPIService;
use OCP\AppFramework\Services\IInitialState;
use OCP\Collaboration\Reference\RenderReferenceEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IConfig;
use OCP\Util;

/**
* @template-implements IEventListener<Event>
*/
class OpenProjectReferenceListener implements IEventListener {

/**
* @var IInitialState
*/
private $initialStateService;

/**
* @var IConfig
*/
private $config;

public function __construct(
IInitialState $initialStateService,
IConfig $config
) {
$this->initialStateService = $initialStateService;
$this->config = $config;
}
public function handle(Event $event): void {
// @phpstan-ignore-next-line - make phpstan not complain in nextcloud version other than 26
if (!$event instanceof RenderReferenceEvent) {
return;
}

Util::addScript(Application::APP_ID, Application::APP_ID . '-reference');
$this->initialStateService->provideInitialState('admin-config-status', OpenProjectAPIService::isAdminConfigOk($this->config));

$this->initialStateService->provideInitialState(
'openproject-url',
$this->config->getAppValue(Application::APP_ID, 'openproject_instance_url')
);
}
}
11 changes: 2 additions & 9 deletions lib/Reference/WorkPackageReferenceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
use OCP\IL10N;
use OCP\IURLGenerator;

class WorkPackageReferenceProvider extends ADiscoverableReferenceProvider implements ISearchableReferenceProvider {
class WorkPackageReferenceProvider extends ADiscoverableReferenceProvider {
private const RICH_OBJECT_TYPE = Application::APP_ID . '_work_package';

// as we know we are on NC >= 26, we can use Php 8 syntax for class attributes
Expand Down Expand Up @@ -76,13 +76,6 @@ public function getIconUrl(): string {
);
}

/**
* @inheritDoc
*/
public function getSupportedSearchProviderIds(): array {
return ['openproject-search'];
}

/**
* Parse a link to find a work package ID
*
Expand Down Expand Up @@ -124,7 +117,7 @@ public function matchReference(string $referenceText): bool {
* @inheritDoc
*/
public function resolveReference(string $referenceText): ?IReference {
if ($this->matchReference($referenceText)) {
if ($this->matchReference($referenceText) && OpenProjectAPIService::isAdminConfigOk($this->config)) {
$wpId = $this->getWorkPackageIdFromUrl($referenceText);
if ($wpId !== null) {
$wpInfo = $this->openProjectAPIService->getWorkPackageInfo($this->userId, $wpId);
Expand Down
20 changes: 5 additions & 15 deletions lib/Search/OpenProjectSearchProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,24 +110,14 @@ public function search(IUser $user, ISearchQuery $query): SearchResult {
$offset = $offset ? intval($offset) : 0;
$openprojectUrl = OpenProjectAPIService::sanitizeUrl($this->config->getAppValue(Application::APP_ID, 'openproject_instance_url'));
$accessToken = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'token');

if ($accessToken === '') {
$searchEnabled = $this->config->getUserValue(
$user->getUID(),
Application::APP_ID, 'search_enabled',
$this->config->getAppValue(Application::APP_ID, 'default_enable_unified_search', '0')) === '1';
if ($accessToken === '' || !$searchEnabled) {
return SearchResult::paginated($this->getName(), [], 0);
}

$routeFrom = $query->getRoute();
$requestedFromSmartPicker = $routeFrom === '' || $routeFrom === 'smart-picker';

if (!$requestedFromSmartPicker) {
$searchEnabled = $this->config->getUserValue(
$user->getUID(),
Application::APP_ID, 'search_enabled',
$this->config->getAppValue(Application::APP_ID, 'default_enable_unified_search', '0')) === '1';
if (!$searchEnabled) {
return SearchResult::paginated($this->getName(), [], 0);
}
}

$searchResults = $this->service->searchWorkPackage($user->getUID(), $term, null, false);
$searchResults = array_slice($searchResults, $offset, $limit);

Expand Down
13 changes: 8 additions & 5 deletions lib/Service/OpenProjectAPIService.php
Original file line number Diff line number Diff line change
Expand Up @@ -1151,11 +1151,14 @@ public function hasAppPassword(): bool {
*/
public function getWorkPackageInfo(string $userId, int $wpId): array {
$result[] = null;
$searchResult = $this->searchWorkPackage($userId, null, null, false, $wpId);
$result['title'] = $this->getSubline($searchResult[0]);
$result['description'] = $this->getMainText($searchResult[0]);
$result['imageUrl'] = $this->getOpenProjectUserAvatarUrl($searchResult[0]);
$result['entry'] = $searchResult[0];
$accessToken = $this->config->getUserValue($userId, Application::APP_ID, 'token');
if ($accessToken) {
$searchResult = $this->searchWorkPackage($userId, null, null, false, $wpId);
$result['title'] = $this->getSubline($searchResult[0]);
$result['description'] = $this->getMainText($searchResult[0]);
$result['imageUrl'] = $this->getOpenProjectUserAvatarUrl($searchResult[0]);
$result['entry'] = $searchResult[0];
}
return $result;
}

Expand Down
31 changes: 31 additions & 0 deletions src/components/icons/OpenProjectIcon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<template>
<span :aria-hidden="!title"
:aria-label="title"
class="material-design-icon openproject-icon"
role="img"
v-bind="$attrs"
@click="$emit('click', $event)">
<svg xmlns="http://www.w3.org/2000/svg"
width="150"
height="150">
<path :stroke="fillColor" :fill="fillColor" d="m 80,110 h 0.013 C 80.01,110 80,110.086 80,110.166 c 0,4.01 3.357,7.224 7.5,7.224 4.143,0 7.5,-3.194 7.5,-7.204 C 95,110.104 94.99,110 94.987,110 H 95 V 92 H 80 Z" />
<path :stroke="fillColor" :fill="fillColor" d="M 115,13 H 105 C 91.193,13 80,24.045 80,37.853 v 11 9 V 68 H 46 45 36 C 22.193,68 11,79.046 11,92.853 v 20 C 11,126.659 22.193,138 36,138 h 10 c 13.807,0 25,-11.341 25,-25.147 v -20 C 71,92.518 70.988,92 70.975,92 H 56 v 0.853 7 13 C 56,118.366 51.514,123 46,123 H 36 c -5.514,0 -10,-4.634 -10,-10.147 v -20 C 26,87.339 30.486,83 36,83 h 8 1 1 22.914 2.086 34 7 3 c 13.807,0 25,-11.341 25,-25.147 v -20 C 140,24.045 128.807,13 115,13 Z m 10,44.853 C 125,63.367 120.514,68 115,68 h -3 -3 -4 -10 v -10.147 -9 -1 -10 C 95,32.338 99.486,28 105,28 h 10 c 5.514,0 10,4.338 10,9.853 z" />
</svg>
</span>
</template>

<script>
export default {
name: 'OpenProjectIcon',
props: {
title: {
type: String,
default: '',
},
fillColor: {
type: String,
default: 'currentColor',
},
},
}
</script>
20 changes: 14 additions & 6 deletions src/components/tab/EmptyContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
<div class="empty-content--wrapper">
<div class="empty-content--icon">
<CheckIcon v-if="isStateOk && dashboard" :size="60" />
<LinkPlusIcon v-else-if="!!isAdminConfigOk && isStateOk && !dashboard" :size="60" />
<LinkPlusIcon v-else-if="!!isAdminConfigOk && isStateOk && !dashboard && !isSmartPicker" :size="60" />
<OpenProjectIcon v-else-if="!!isSmartPicker" class="empty-content--icon--openproject" />
<LinkOffIcon v-else :size="60" />
</div>
<div v-if="!!isAdminConfigOk" class="empty-content--message">
<div v-if="!!isAdminConfigOk && !isSmartPicker" class="empty-content--message">
<div class="empty-content--message--title">
{{ emptyContentTitleMessage }}
</div>
Expand All @@ -25,14 +26,15 @@
import LinkPlusIcon from 'vue-material-design-icons/LinkPlus.vue'
import LinkOffIcon from 'vue-material-design-icons/LinkOff.vue'
import CheckIcon from 'vue-material-design-icons/Check.vue'
import OpenProjectIcon from '../icons/OpenProjectIcon.vue'
import { generateUrl } from '@nextcloud/router'
import { translate as t } from '@nextcloud/l10n'
import OAuthConnectButton from '../OAuthConnectButton.vue'
import { STATE } from '../../utils.js'
export default {
name: 'EmptyContent',
components: { OAuthConnectButton, LinkPlusIcon, LinkOffIcon, CheckIcon },
components: { OAuthConnectButton, LinkPlusIcon, LinkOffIcon, CheckIcon, OpenProjectIcon },
props: {
state: {
type: String,
Expand All @@ -57,6 +59,10 @@ export default {
type: Boolean,
default: false,
},
isSmartPicker: {
type: Boolean,
default: false,
},
},
data() {
return {
Expand Down Expand Up @@ -109,9 +115,11 @@ export default {
display: flex;
align-items: center;
justify-content: center;
img {
height: 50px;
width: 50px;
&--openproject {
margin: 90px;
display: block;
align-items: center;
justify-content: center;
}
}
&--message {
Expand Down
52 changes: 41 additions & 11 deletions src/components/tab/SearchInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
</template>

<script>
import { loadState } from '@nextcloud/initial-state'
import debounce from 'lodash/debounce.js'
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
Expand All @@ -50,18 +51,22 @@ export default {
props: {
fileInfo: {
type: Object,
required: true,
default: null,
},
linkedWorkPackages: {
type: Array,
required: true,
default: null,
},
isSmartPicker: {
type: Boolean,
default: false,
},
},
data: () => ({
state: STATE.OK,
searchResults: [],
noOptionsText: t('integration_openproject', 'Start typing to search'),
placeholder: t('integration_openproject', 'Search for a work package to create a relation'),
openprojectUrl: loadState('integration_openproject', 'openproject-url'),
}),
computed: {
isStateOk() {
Expand All @@ -78,8 +83,18 @@ export default {
}
return ''
},
placeholder() {
if (this.isSmartPicker) {
return t('integration_openproject', 'Search for work packages')
} else {
return t('integration_openproject', 'Search for a work package to create a relation')
}
},
filterSearchResultsByFileId() {
return this.searchResults.filter(wp => {
if (this.isSmartPicker) {
return wp.id
}
if (wp.fileId === undefined || wp.fileId === '') {
console.error('work-package data does not contain a fileId')
return false
Expand Down Expand Up @@ -117,13 +132,21 @@ export default {
},
async asyncFind(query) {
this.resetState()
await this.debounceMakeSearchRequest(query, this.fileInfo.id)
await this.debounceMakeSearchRequest(query, this.fileInfo.id, this.isSmartPicker)
},
async getWorkPackageLink(selectedOption) {
return this.openprojectUrl + '/projects/' + selectedOption.projectId + '/work_packages/' + selectedOption.id
},
debounceMakeSearchRequest: debounce(function(...args) {
if (args[0].length < SEARCH_CHAR_LIMIT) return
return this.makeSearchRequest(...args)
}, DEBOUNCE_THRESHOLD),
async linkWorkPackageToFile(selectedOption) {
if (this.isSmartPicker) {
const link = await this.getWorkPackageLink(selectedOption)
this.$emit('submit', link)
return
}
const params = new URLSearchParams()
params.append('workpackageId', selectedOption.id)
params.append('fileId', this.fileInfo.id)
Expand All @@ -149,12 +172,13 @@ export default {
)
}
},
async makeSearchRequest(search, fileId) {
async makeSearchRequest(search, fileId = null, isSmartPicker = false) {
this.state = STATE.LOADING
const url = generateUrl('/apps/integration_openproject/work-packages')
const req = {}
req.params = {
searchQuery: search,
isSmartPicker,
}
let response
try {
Expand All @@ -170,13 +194,19 @@ export default {
for (let workPackage of workPackages) {
try {
if (this.isStateLoading) {
workPackage.fileId = fileId
workPackage = await workpackageHelper.getAdditionalMetaData(workPackage)
const alreadyLinked = this.linkedWorkPackages.some(el => el.id === workPackage.id)
const alreadyInSearchResults = this.searchResults.some(el => el.id === workPackage.id)
// check the state again, it might have changed in between
if (!alreadyInSearchResults && !alreadyLinked && this.isStateLoading) {
if (this.isSmartPicker) {
workPackage = await workpackageHelper.getAdditionalMetaData(workPackage)
this.searchResults.push(workPackage)

} else {
workPackage.fileId = fileId
workPackage = await workpackageHelper.getAdditionalMetaData(workPackage)
const alreadyLinked = this.linkedWorkPackages.some(el => el.id === workPackage.id)
const alreadyInSearchResults = this.searchResults.some(el => el.id === workPackage.id)
// check the state again, it might have changed in between
if (!alreadyInSearchResults && !alreadyLinked && this.isStateLoading) {
this.searchResults.push(workPackage)
}
}
}
} catch (e) {
Expand Down
19 changes: 18 additions & 1 deletion src/reference.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
*/

// this requires @nextcloud/vue >= 7.9.0
import { registerWidget } from '@nextcloud/vue/dist/Components/NcRichText.js'
import { registerWidget, registerCustomPickerElement, NcCustomPickerRenderResult } from '@nextcloud/vue/dist/Components/NcRichText.js'

// this is required for lazy loading
__webpack_nonce__ = btoa(OC.requestToken) // eslint-disable-line
Expand All @@ -41,3 +41,20 @@ registerWidget('integration_openproject_work_package', async (el, { richObjectTy
},
}).$mount(el)
})

registerCustomPickerElement('openproject-work-package-ref', async (el, { providerId, accessible }) => {
const { default: Vue } = await import(/* webpackChunkName: "reference-picker-lazy" */'vue')
const { default: WorkPackagePickerElement } = await import(/* webpackChunkName: "reference-picker-lazy" */'./views/WorkPackagePickerElement.vue')
Vue.mixin({ methods: { t, n } })

const Element = Vue.extend(WorkPackagePickerElement)
const vueElement = new Element({
propsData: {
providerId,
accessible,
},
}).$mount(el)
return new NcCustomPickerRenderResult(vueElement.$el, vueElement)
}, (el, renderResult) => {
renderResult.object.$destroy()
})
Loading
Loading