├── .eslintrc.js ├── .github ├── CODEOWNERS ├── CODEOWNERS.license ├── pull_request_template.md ├── pull_request_template.md.license ├── scripts │ ├── generate-matrix.sh │ └── notify-to-element.sh └── workflows │ ├── ci-workflow.yml │ ├── nighlty-ci-release-branch.yml │ ├── pr-feedback.yml │ ├── release.yml │ ├── reuse.yml │ └── shared_workflow.yml ├── .gitignore ├── .l10nignore ├── .php-cs-fixer.dist.php ├── .tx ├── backport └── config ├── AUTHORS.md ├── CHANGELOG.md ├── COPYING ├── LICENSES ├── AGPL-3.0-only.txt ├── AGPL-3.0-or-later.txt ├── Apache-2.0.txt ├── CC0-1.0.txt ├── LicenseRef-OpenProjectTrademarks.txt └── MIT.txt ├── README.md ├── REUSE.toml ├── appinfo ├── info.xml └── routes.php ├── babel.config.js ├── bootstrap.php ├── composer.json ├── composer.lock ├── css ├── dashboard.css └── tab.css ├── dev ├── .env ├── apps │ └── .keep ├── compose.yaml ├── keycloak.yaml ├── nextcloud.sh ├── nextcloud │ ├── config.php │ └── hooks │ │ ├── after-install.sh │ │ └── before-install.sh ├── openproject.sh ├── opnc-realm.json ├── opnc-realm.json.license ├── ssl.sh └── step.sh ├── docker-compose.override.example.yml ├── docker-compose.yml ├── docs ├── direct_upload.md ├── get_file_information.md ├── release.md ├── running_API_test.md ├── setting_up_as_admin.md ├── setting_up_via_shell_script.md ├── setup_nc_op__full.md ├── setup_via_docker.md └── testing │ └── smoke_testing.md ├── img ├── app-dark.svg ├── app.svg ├── bell-ring-dark.svg ├── bell-ring.svg ├── screenshot1.jpg ├── screenshot1.jpg.license ├── screenshot1.png ├── screenshot1.png.license ├── screenshot2.jpg ├── screenshot2.jpg.license ├── screenshot2.png └── screenshot2.png.license ├── integration_oidc_setup.sh ├── integration_setup.sh ├── jest.config.js ├── l10n ├── .gitkeep ├── ar.js ├── ar.json ├── ast.js ├── ast.json ├── bg.js ├── bg.json ├── ca.js ├── ca.json ├── cs.js ├── cs.json ├── da.js ├── da.json ├── de.js ├── de.json ├── de_DE.js ├── de_DE.json ├── el.js ├── el.json ├── en_GB.js ├── en_GB.json ├── es.js ├── es.json ├── es_EC.js ├── es_EC.json ├── es_MX.js ├── es_MX.json ├── et_EE.js ├── et_EE.json ├── eu.js ├── eu.json ├── fa.js ├── fa.json ├── fi.js ├── fi.json ├── fr.js ├── fr.json ├── ga.js ├── ga.json ├── gl.js ├── gl.json ├── he.js ├── he.json ├── hr.js ├── hr.json ├── hu.js ├── hu.json ├── id.js ├── id.json ├── is.js ├── is.json ├── it.js ├── it.json ├── ja.js ├── ja.json ├── ka.js ├── ka.json ├── ko.js ├── ko.json ├── lt_LT.js ├── lt_LT.json ├── lv.js ├── lv.json ├── mk.js ├── mk.json ├── nb.js ├── nb.json ├── nl.js ├── nl.json ├── oc.js ├── oc.json ├── pl.js ├── pl.json ├── pt_BR.js ├── pt_BR.json ├── pt_PT.js ├── pt_PT.json ├── ro.js ├── ro.json ├── ru.js ├── ru.json ├── sc.js ├── sc.json ├── sk.js ├── sk.json ├── sl.js ├── sl.json ├── sr.js ├── sr.json ├── sv.js ├── sv.json ├── th.js ├── th.json ├── tr.js ├── tr.json ├── ug.js ├── ug.json ├── uk.js ├── uk.json ├── vi.js ├── vi.json ├── zh_CN.js ├── zh_CN.json ├── zh_HK.js ├── zh_HK.json ├── zh_TW.js └── zh_TW.json ├── lib ├── AppInfo │ └── Application.php ├── BackgroundJob │ └── RemoveExpiredDirectUploadTokens.php ├── Capabilities.php ├── Controller │ ├── ConfigController.php │ ├── DirectDownloadController.php │ ├── DirectUploadController.php │ ├── FilesController.php │ ├── OpenProjectAPIController.php │ └── OpenProjectController.php ├── Dashboard │ └── OpenProjectWidget.php ├── Exception │ ├── OpenprojectAvatarErrorException.php │ ├── OpenprojectErrorException.php │ ├── OpenprojectFileNotUploadedException.php │ ├── OpenprojectGroupfolderSetupConflictException.php │ ├── OpenprojectResponseException.php │ └── OpenprojectUnauthorizedUserException.php ├── Listener │ ├── BeforeGroupDeletedListener.php │ ├── BeforeNodeInsideOpenProjectGroupfilderChangedListener.php │ ├── BeforeUserDeletedListener.php │ ├── LoadAdditionalScriptsListener.php │ ├── LoadSidebarScript.php │ ├── OpenProjectReferenceListener.php │ ├── TermsOfServiceEventListener.php │ └── UserChangedListener.php ├── Migration │ ├── Version2001Date20221213083550.php │ ├── Version2310Date20230116153411.php │ ├── Version2400Date20230504144300.php │ └── Version2640Date20240628114301.php ├── OIDCClientMapper.php ├── Reference │ └── WorkPackageReferenceProvider.php ├── Search │ ├── OpenProjectSearchProvider.php │ └── OpenProjectSearchResultEntry.php ├── ServerVersionHelper.php ├── Service │ ├── DatabaseService.php │ ├── DirectDownloadService.php │ ├── DirectUploadService.php │ ├── OauthService.php │ ├── OpenProjectAPIService.php │ └── SettingsService.php ├── Settings │ ├── Admin.php │ ├── AdminSection.php │ ├── Personal.php │ └── PersonalSection.php └── TokenEventFactory.php ├── makefile ├── package-lock.json ├── package.json ├── phpunit.xml ├── psalm.xml ├── src ├── adminSettings.js ├── api │ ├── endpoints.js │ └── settings.js ├── bootstrap.js ├── components │ ├── AdminSettings.vue │ ├── ErrorLabel.vue │ ├── OAuthConnectButton.vue │ ├── PersonalSettings.vue │ ├── admin │ │ ├── FieldValue.vue │ │ ├── FormHeading.vue │ │ ├── FormOpenProjectHost.vue │ │ ├── TermsOfServiceUnsigned.vue │ │ └── TextInput.vue │ ├── icons │ │ ├── ClippyIcon.vue │ │ └── OpenProjectIcon.vue │ ├── settings │ │ ├── CheckBox.vue │ │ ├── ErrorNote.vue │ │ └── SettingsTitle.vue │ └── tab │ │ ├── EmptyContent.vue │ │ ├── SearchInput.vue │ │ └── WorkPackage.vue ├── constants │ ├── appID.js │ ├── links.js │ └── messages.js ├── dashboard.js ├── fileActions.js ├── filesPlugin │ ├── filesPlugin.js │ └── filesPluginLessThan28.js ├── personalSettings.js ├── projectTab.js ├── reference.js ├── utils.js ├── utils │ └── workpackageHelper.js └── views │ ├── CreateWorkPackageModal.vue │ ├── Dashboard.vue │ ├── LinkMultipleFilesModal.vue │ ├── ProjectsTab.vue │ ├── WorkPackagePickerElement.vue │ └── WorkPackageReferenceWidget.vue ├── stylelint.config.js ├── templates ├── adminSettings.php └── personalSettings.php ├── testplan.md ├── tests ├── acceptance │ ├── config │ │ └── behat.yml │ └── features │ │ ├── api │ │ ├── capabilities.feature │ │ ├── directUpload.feature │ │ ├── directUploadPrepare.feature │ │ ├── getFileinfoByFileIDAPI.feature │ │ ├── getFilesinfoByFileIDsAPI.feature │ │ └── setup.feature │ │ ├── bootstrap │ │ ├── DirectUploadContext.php │ │ ├── FeatureContext.php │ │ ├── FilesVersionsContext.php │ │ ├── GroupfoldersContext.php │ │ └── SharingContext.php │ │ └── searchBar.feature ├── jest │ ├── __mocks__ │ │ └── @nextcloud │ │ │ ├── l10n.js │ │ │ └── router.js │ ├── components │ │ ├── AdminSettings.spec.js │ │ ├── OAuthConnectButton.spec.js │ │ ├── PersonalSettings.spec.js │ │ ├── __snapshots__ │ │ │ ├── AdminSettings.spec.js.snap │ │ │ ├── OAuthConnectButton.spec.js.snap │ │ │ └── PersonalSettings.spec.js.snap │ │ ├── admin │ │ │ ├── FieldValue.spec.js │ │ │ ├── FormHeading.spec.js │ │ │ ├── FormOpenProjectHost.spec.js │ │ │ ├── TermsOfServices.spec.js │ │ │ ├── TextInput.spec.js │ │ │ └── __snapshots__ │ │ │ │ ├── FieldValue.spec.js.snap │ │ │ │ ├── FormHeading.spec.js.snap │ │ │ │ ├── FormOpenProjectHost.spec.js.snap │ │ │ │ └── TextInput.spec.js.snap │ │ ├── settings │ │ │ ├── ErrorNote.spec.js │ │ │ └── __snapshots__ │ │ │ │ └── ErrorNote.spec.js.snap │ │ └── tab │ │ │ ├── EmptyContent.spec.js │ │ │ ├── SearchInput.spec.js │ │ │ ├── WorkPackage.spec.js │ │ │ └── __snapshots__ │ │ │ ├── EmptyContent.spec.js.snap │ │ │ ├── SearchInput.spec.js.snap │ │ │ └── WorkPackage.spec.js.snap │ ├── fixtures │ │ ├── availableProjectAssigneesResponse.json │ │ ├── availableProjectOptions.json │ │ ├── formValidationResponseRequiredType.json │ │ ├── notificationsResponse.json │ │ ├── openprojectAvailableProjectResponse.json │ │ ├── openprojectAvailableProjectResponseAfterSearch.json │ │ ├── workPackageObjectsInSearchResults.json │ │ ├── workPackageSearchReqResponse.json │ │ ├── workPackageSuccessfulCreationResponse.json │ │ ├── workPackagesSearchResponse.json │ │ ├── workPackagesSearchResponseNoAssignee.json │ │ ├── workpackageFormValidationProjectSelectedResponse.json │ │ └── workpackageFormValidationTypeChanged.json │ ├── global.mock.js │ ├── stubs │ │ └── empty.js │ └── views │ │ ├── CreateWorkpackageModal.spec.js │ │ ├── Dashboard.spec.js │ │ ├── LinkMultipleFilesModal.spec.js │ │ ├── ProjectsTab.spec.js │ │ └── __snapshots__ │ │ ├── CreateWorkpackageModal.spec.js.snap │ │ ├── Dashboard.spec.js.snap │ │ ├── LinkMultipleFilesModal.spec.js.snap │ │ └── ProjectsTab.spec.js.snap ├── lib │ ├── Controller │ │ ├── ConfigControllerTest.php │ │ ├── DirectUploadControllerTest.php │ │ ├── FilesControllerTest.php │ │ ├── OpenProjectAPIControllerTest.php │ │ └── OpenProjectControllerTest.php │ ├── Reference │ │ └── WorkPackageReferenceProviderTest.php │ ├── Service │ │ ├── DatabaseServiceTest.php │ │ ├── DirectDownloadServiceTest.php │ │ ├── DirectUploadServiceTest.php │ │ ├── OauthSeviceTest.php │ │ ├── OpenProjectAPIServiceTest.php │ │ └── SettingsServiceTest.php │ ├── Settings │ │ └── PersonalTest.php │ ├── TokenEventFactoryTest.php │ └── fixtures │ │ ├── openproject-icon.jpg │ │ └── openproject-icon.jpg.license └── stub │ ├── doctrine_cacheItem.phpstub │ └── timejob_joblist.phpstub └── webpack.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2021-2022 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | module.exports = { 7 | globals: { 8 | appVersion: true 9 | }, 10 | parserOptions: { 11 | requireConfigFile: false 12 | }, 13 | extends: [ 14 | '@nextcloud' 15 | ], 16 | rules: { 17 | 'jsdoc/require-jsdoc': 'off', 18 | 'jsdoc/tag-lines': 'off', 19 | 'vue/first-attribute-linebreak': 'off', 20 | 'import/extensions': 'off' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @julien-nc @individual-it @akabiru @ba1ash @dominic-braeunlein @Kharonus @lindenthal @nabim777 @psatyal @saw-jan @wielinde @NobodysNightmare 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Jankari Tech Pvt. Ltd. 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | 5 | ## Related Issue or Workpackage 6 | 7 | 8 | 9 | 10 | - Fixes 11 | 12 | ## Screenshots (if appropriate): 13 | 14 | 15 | ## Types of changes 16 | 17 | - [ ] Bug fix (non-breaking change which fixes an issue) 18 | - [ ] New feature (non-breaking change which adds functionality) 19 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 20 | - [ ] Tests only (no source changes) 21 | 22 | ## Checklist: 23 | 24 | - [ ] Code changes 25 | - [ ] Unit tests added 26 | - [ ] Acceptance tests added 27 | - [ ] Updated `CHANGELOG.md` file 28 | -------------------------------------------------------------------------------- /.github/pull_request_template.md.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Jankari Tech Pvt. Ltd. 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | -------------------------------------------------------------------------------- /.github/scripts/notify-to-element.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # SPDX-FileCopyrightText: 2023-2024 Jankari Tech Pvt. Ltd. 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | # helper functions 6 | log_error() { 7 | echo -e "\e[31m$1\e[0m" 8 | } 9 | 10 | log_info() { 11 | echo -e "\e[37m$1\e[0m" 12 | } 13 | 14 | log_success() { 15 | echo -e "\e[32m$1\e[0m" 16 | } 17 | 18 | log_info "Fetching all workflow jobs....." 19 | 20 | response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ 21 | "https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/actions/runs/$RUN_ID/jobs?per_page=50") 22 | 23 | log_info "Fetching jobs informations succeeded! 24 | " 25 | if [[ "$response" != *"jobs"* ]]; then 26 | log_error "No jobs found in the below response!" 27 | log_info "$response" 28 | exit 1 29 | fi 30 | 31 | jobs_informations=$(echo "$response" | jq '.jobs[:-1]') 32 | jobs_conclusions=$(echo "$jobs_informations" | jq -r '.[].conclusion') 33 | 34 | workflow_status="Success" 35 | if [[ " ${jobs_conclusions[*]} " == *"failure"* ]]; then 36 | workflow_status="Failure" 37 | elif [[ " ${jobs_conclusions[*]} " == *"cancelled"* ]]; then 38 | workflow_status="Cancelled" 39 | elif [[ " ${jobs_conclusions[*]} " == *"skipped"* ]]; then 40 | workflow_status="Skipped" 41 | fi 42 | 43 | log_info "Sending report to the element chat...." 44 | 45 | send_message_to_room_response=$(curl -s -XPOST "$ELEMENT_CHAT_URL/_matrix/client/r0/rooms/%21$ELEMENT_ROOM_ID/send/m.room.message?access_token=$NIGHTLY_CI_USER_TOKEN" \ 46 | -d ' 47 | { 48 | "msgtype": "m.text", 49 | "body": "", 50 | "format": "org.matrix.custom.html", 51 | "formatted_body": "NC-Nightly-'$BRANCH_NAME'

Status: '$workflow_status'" 52 | } 53 | ' 54 | ) 55 | 56 | if [[ "$send_message_to_room_response" != *"event_id"* ]]; then 57 | log_error "Failed to send message to element. Below response did not contain event_id!" 58 | log_info "$send_message_to_room_response" 59 | exit 1 60 | fi 61 | 62 | log_success "Notification of the nightly build has been sent to Element chat (OpenProject + Nextcloud)" 63 | -------------------------------------------------------------------------------- /.github/workflows/ci-workflow.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Jankari Tech Pvt. Ltd. 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | name: CI 4 | 5 | on: 6 | push: 7 | branches: 8 | - 'master' 9 | pull_request: 10 | paths-ignore: 11 | - '**.md' 12 | - '**.txt' 13 | - '**.sh' 14 | - 'dev/**' 15 | - 'l10n/**' 16 | - 'img/**' 17 | - 'docker-compose*' 18 | schedule: 19 | - cron: '0 22 * * *' # run at 10 PM UTC 20 | 21 | concurrency: 22 | group: ${{ github.workflow }}-${{ github.ref }} 23 | cancel-in-progress: true 24 | 25 | jobs: 26 | builds: 27 | uses: ./.github/workflows/shared_workflow.yml 28 | secrets: inherit 29 | with: 30 | branch: master 31 | -------------------------------------------------------------------------------- /.github/workflows/nighlty-ci-release-branch.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023-2025 Jankari Tech Pvt. Ltd. 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | name: Nightly CI (Release branch) 4 | 5 | # workflow can be scheduled ONLY from DEFAULT branch 6 | # > This event will only trigger a workflow run if the workflow file is on the default branch. 7 | # See: https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule 8 | on: 9 | schedule: 10 | - cron: '0 23 * * *' # run at 11 PM UTC 11 | 12 | jobs: 13 | builds: 14 | uses: ./.github/workflows/shared_workflow.yml 15 | secrets: inherit 16 | with: 17 | branch: release/2.9 18 | nextcloud_versions: "30 31" 19 | -------------------------------------------------------------------------------- /.github/workflows/reuse.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | 6 | # SPDX-FileCopyrightText: 2022 Free Software Foundation Europe e.V. 7 | # 8 | # SPDX-License-Identifier: CC0-1.0 9 | 10 | name: REUSE Compliance Check 11 | 12 | on: [pull_request] 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | reuse-compliance-check: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 23 | with: 24 | persist-credentials: false 25 | 26 | - name: REUSE Compliance Check 27 | uses: fsfe/reuse-action@bb774aa972c2a89ff34781233d275075cbddf542 # v5.0.0 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022-2025 Jankari Tech Pvt. Ltd. 2 | # SPDX-FileCopyrightText: 2021-2023 Nextcloud GmbH and Nextcloud contributors 3 | # SPDX-FileCopyrightText: 2022 OpenProject GmbH 4 | # SPDX-License-Identifier: AGPL-3.0-or-later 5 | js/ 6 | .code-workspace 7 | .DS_Store 8 | .idea/ 9 | .vscode/ 10 | .vscode-upload.json 11 | .*.sw* 12 | node_modules 13 | .phpunit.result.cache 14 | coverage/ 15 | example/ 16 | log/ 17 | vendor/ 18 | .php-cs-fixer.cache 19 | tests/pact/ 20 | docker-compose.override.yml 21 | .env 22 | 23 | # VSCode 24 | .vscode 25 | *.code-workspace 26 | 27 | # dev environment 28 | !dev/.env 29 | dev/apps 30 | dev/certs 31 | -------------------------------------------------------------------------------- /.l10nignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # compiled vue templates 4 | js/ 5 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | getFinder() 18 | ->ignoreVCSIgnored(true) 19 | ->notPath('build') 20 | ->notPath('l10n') 21 | ->notPath('src') 22 | ->notPath('vendor') 23 | ->notPath('server') 24 | ->notPath('lib/Reference/WorkPackageReferenceProvider.php') 25 | ->in(__DIR__); 26 | return $config; 27 | -------------------------------------------------------------------------------- /.tx/backport: -------------------------------------------------------------------------------- 1 | release/2.0 2 | release/2.1 3 | release/2.2 4 | release/2.3 5 | release/2.4 6 | release/2.5 7 | release/2.6 8 | release/2.7 9 | release/2.8 10 | release/2.9 11 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | lang_map = nb_NO: nb, sk_SK: sk, th_TH: th, ja_JP: ja, bg_BG: bg, cs_CZ: cs, fi_FI: fi, hu_HU: hu 4 | 5 | [o:nextcloud:p:nextcloud:r:integration_openproject] 6 | file_filter = translationfiles//integration_openproject.po 7 | source_file = translationfiles/templates/integration_openproject.pot 8 | source_lang = en 9 | type = PO 10 | 11 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | 5 | # Authors 6 | 7 | - Andy Scherzinger 8 | - Artur Neumann 9 | - Christophe Bliard 10 | - Dominic Bräunlein 11 | - Eric Schubert <38206611+Kharonus@users.noreply.github.com> 12 | - Ferdinand Thiessen 13 | - Joas Schilling 14 | - Julien Veyssier 15 | - Julius Knorr 16 | - Jérémie Tarot 17 | - Kiran Parajuli 18 | - Louis 19 | - nabim777 / Nalem7 20 | - Parimal Satyal <88370597+psatyal@users.noreply.github.com> 21 | - Phil Davis 22 | - q-wertz 23 | - rakekniven <2069590+rakekniven@users.noreply.github.com> 24 | - Rello 25 | - Sagar Gurung 26 | - Sawjan Gurung 27 | - Swikriti Tripathi 28 | - Valdnet <47037905+Valdnet@users.noreply.github.com> 29 | - Wieland Lindenthal 30 | -------------------------------------------------------------------------------- /LICENSES/LicenseRef-OpenProjectTrademarks.txt: -------------------------------------------------------------------------------- 1 | The OpenProject marks 2 | OpenProject Icon logo is _of_unknown_license_to_be_clarified_ (a registered trademark of OpenProject GmbH in Germany and/or other countries?). 3 | 4 | A copy can be found at https://www.openproject.org/press/ 5 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | associated documentation files (the "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial 12 | portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 15 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 16 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 18 | USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 7 | # 🔗 OpenProject Integration 8 | 9 | [![REUSE status](https://api.reuse.software/badge/github.com/nextcloud/integration_openproject)](https://api.reuse.software/info/github.com/nextcloud/integration_openproject) 10 | 11 | This application enables integration between Nextcloud and open-source project management software OpenProject. 12 | 13 | ![](https://github.com/nextcloud/integration_openproject/raw/master/img/screenshot1.png) 14 | ![](https://github.com/nextcloud/integration_openproject/raw/master/img/screenshot2.png) 15 | 16 | On the Nextcloud end, it allows users to: 17 | 18 | * Link files and folders with work packages in OpenProject 19 | * Find all work packages linked to a file or a folder 20 | * Create work packages directly in Nextcloud 21 | * View OpenProject notifications via the dashboard 22 | * Search for work packages using Nextcloud's search bar 23 | * Link work packages in rich text fields via Smart Picker 24 | * Preview links to work packages in text fields 25 | * Link multiple files and folders to a work package at once 26 | 27 | On the OpenProject end, users are able to: 28 | 29 | * View all Nextcloud files and folders linked to a work package 30 | * Download linked files or open them in Nextcloud to edit them 31 | * Open linked files in Nextcloud to edit them 32 | * Let OpenProject create shared folders per project 33 | 34 | Please report issues and bugs here: https://community.openproject.org/projects/nextcloud-integration/work_packages 35 | 36 | ## 📚 Documentation for users and administrators guide 37 | - For documentation on how to set up the integration as an administrator, refer to [Nextcloud integration setup](https://openproject.org/docs/system-admin-guide/integrations/nextcloud/). 38 | - For documentation on how to use the integration once it is set up, refer to [Using the Nextcloud integration](https://openproject.org/docs/user-guide/nextcloud-integration/). 39 | 40 | ## 🔨 Development Setup Guide 41 | - [Set up via docker](docs/setup_via_docker.md) 42 | - [Start Nextcloud-OpenProject full setup](docs/setup_nc_op__full.md) 43 | - [APIs for integration setup for admin](docs/setting_up_as_admin.md) 44 | - [Setting up Integration via Shell Script](docs/setting_up_via_shell_script.md) 45 | - [APIs for Direct Upload](docs/direct_upload.md) 46 | - [APIs to get file information](docs/get_file_information.md) 47 | - [Release Preparation](docs/release.md) 48 | - [Running API tests](docs/running_API_test.md) 49 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | version = 1 4 | SPDX-PackageName = "integration_openproject" 5 | SPDX-PackageSupplier = "Nextcloud " 6 | SPDX-PackageDownloadLocation = "https://github.com/nextcloud/integration_openproject" 7 | 8 | [[annotations]] 9 | path = ["l10n/*.js", "l10n/*.json"] 10 | precedence = "aggregate" 11 | SPDX-FileCopyrightText = "2021 Nextcloud translators" 12 | SPDX-License-Identifier = "AGPL-3.0-or-later" 13 | 14 | [[annotations]] 15 | path = [".tx/config"] 16 | precedence = "aggregate" 17 | SPDX-FileCopyrightText = "2021 Nextcloud GmbH and Nextcloud contributors" 18 | SPDX-License-Identifier = "AGPL-3.0-or-later" 19 | 20 | [[annotations]] 21 | path = [".tx/backport"] 22 | precedence = "aggregate" 23 | SPDX-FileCopyrightText = "2022-2024 Jankari Tech Pvt. Ltd." 24 | SPDX-License-Identifier = "AGPL-3.0-or-later" 25 | 26 | [[annotations]] 27 | path = ["composer.json", "composer.lock"] 28 | precedence = "aggregate" 29 | SPDX-FileCopyrightText = "2022-2025 Jankari Tech Pvt. Ltd., 2022 Nextcloud GmbH and Nextcloud contributors" 30 | SPDX-License-Identifier = "AGPL-3.0-or-later" 31 | 32 | [[annotations]] 33 | path = ["package-lock.json", "package.json"] 34 | precedence = "aggregate" 35 | SPDX-FileCopyrightText = "2021-2025 Nextcloud GmbH and Nextcloud contributors, 2022-2024 Jankari Tech Pvt. Ltd." 36 | SPDX-License-Identifier = "AGPL-3.0-or-later" 37 | 38 | [[annotations]] 39 | path = "tests/jest/**/__snapshots__/*.snap" 40 | precedence = "aggregate" 41 | SPDX-FileCopyrightText = "2022-2025 Jankari Tech Pvt. Ltd., 2022-2023 Nextcloud GmbH and Nextcloud contributors" 42 | SPDX-License-Identifier = "AGPL-3.0-or-later" 43 | 44 | [[annotations]] 45 | path = "tests/jest/fixtures/*.json" 46 | precedence = "aggregate" 47 | SPDX-FileCopyrightText = "2022-2024 Jankari Tech Pvt. Ltd." 48 | SPDX-License-Identifier = "AGPL-3.0-or-later" 49 | 50 | [[annotations]] 51 | path = ["img/app.svg", "img/app-dark.svg"] 52 | precedence = "aggregate" 53 | SPDX-FileCopyrightText = "2018-2024 Google LLC" 54 | SPDX-License-Identifier = "LicenseRef-OpenProjectTrademarks" 55 | 56 | [[annotations]] 57 | path = ["img/bell-ring.svg", "img/bell-ring-dark.svg"] 58 | precedence = "aggregate" 59 | SPDX-FileCopyrightText = "2012-2024 OpenProject GmbH" 60 | SPDX-License-Identifier = "Apache-2.0" 61 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // babel.config.js 2 | 3 | /** 4 | * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors 5 | * SPDX-License-Identifier: AGPL-3.0-or-later 6 | */ 7 | module.exports = { 8 | plugins: ['@babel/plugin-syntax-dynamic-import'], 9 | presets: ['@babel/preset-env'], 10 | } 11 | -------------------------------------------------------------------------------- /bootstrap.php: -------------------------------------------------------------------------------- 1 | addPsr4("OCA\\OpenProject\\", __DIR__ . '/lib', true); 24 | $classLoader->addPsr4("OCA\\OpenProject\\Service\\", __DIR__ . '/lib/Service', true); 25 | $classLoader->addPsr4("OCA\\OpenProject\\Settings\\", __DIR__ . '/lib/Settings', true); 26 | $classLoader->addPsr4("OCP\\", $serverPath . '/lib/public', true); 27 | $classLoader->addPsr4("OC\\", $serverPath . '/lib/private', true); 28 | $classLoader->addPsr4("OCA\\Files\\Event\\", $serverPath . '/apps/files/lib/Event', true); 29 | $classLoader->addPsr4("OCA\\OpenProject\\AppInfo\\", __DIR__ . '/lib/AppInfo', true); 30 | $classLoader->addPsr4("OCA\\OpenProject\\Controller\\", __DIR__ . '/lib/Controller', true); 31 | $classLoader->addPsr4("OCA\\OpenProject\\Exception\\", __DIR__ . '/lib/Exception', true); 32 | $classLoader->register(); 33 | 34 | set_include_path(get_include_path() . PATH_SEPARATOR . '/usr/share/php'); 35 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require-dev": { 3 | "phpunit/phpunit": "^9.5", 4 | "pact-foundation/pact-php": "^10.0.0-beta2", 5 | "friendsofphp/php-cs-fixer": "^3.65.0", 6 | "nextcloud/coding-standard": "^1.0", 7 | "behat/behat": "^3.10", 8 | "helmich/phpunit-json-assert": "^3.4", 9 | "vimeo/psalm": "5.23.1", 10 | "guzzlehttp/guzzle": "^7.9", 11 | "behat/gherkin": "v4.12.0", 12 | "php-mock/php-mock-phpunit": "^2.10" 13 | }, 14 | "scripts": { 15 | "cs:fix": "php-cs-fixer fix", 16 | "cs:check": "php-cs-fixer fix --dry-run --diff", 17 | "psalm": "psalm", 18 | "test:unit": "phpunit", 19 | "test:api": "behat -c tests/acceptance/config/behat.yml" 20 | }, 21 | "config": { 22 | "allow-plugins": { 23 | "phpstan/extension-installer": true, 24 | "tienvx/composer-downloads-plugin": true, 25 | "pact-foundation/composer-downloads-plugin": true 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /css/dashboard.css: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2022-2024 Jankari Tech Pvt. Ltd. 3 | * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors 4 | * SPDX-License-Identifier: AGPL-3.0-or-later 5 | */ 6 | 7 | .icon-openproject { 8 | background-image: url('../img/app-dark.svg'); 9 | filter: var(--background-invert-if-dark); 10 | } 11 | 12 | /* for NC <= 24 */ 13 | body.theme--dark .icon-openproject { 14 | background-image: url('../img/app.svg'); 15 | } 16 | 17 | .panel--content .item-list__entry:hover > .avatardiv { 18 | border: 1px solid #dbdbdb; 19 | } 20 | 21 | .panel > .panel--header .icon-openproject { 22 | margin-top: -4px; 23 | position: relative; 24 | } 25 | 26 | .panel--content .empty-content { 27 | margin: 0 !important; 28 | height: 100%; 29 | align-items: start; 30 | justify-content: center; 31 | color: var(--color-text-maxcontrast); 32 | } 33 | 34 | .panel--content .empty-content .empty-content--message--title { 35 | font-weight: normal; 36 | font-size: 100%; 37 | } 38 | 39 | .panel--content .empty-content .empty-content--icon { 40 | opacity: .4; 41 | } 42 | 43 | .panel > .panel--header .icon-openproject::before { 44 | content: ''; 45 | position: absolute; 46 | height: 24px; width: 24px; 47 | background-size: 24px; 48 | top: 3px; 49 | left: 180px; 50 | background-image: url('../img/bell-ring.svg'); 51 | } 52 | 53 | /* there is a default filter for the svgs in the dark mode for NC v25+ 54 | so, we do not have to use the dark svg anymore for NC v25+, 55 | the icon will change the color automatically 56 | but, we have to use the dark svg for NC v24 and below. */ 57 | body.theme--dark .panel > .panel--header .icon-openproject::before { 58 | background-image: url('../img/bell-ring-dark.svg'); 59 | } 60 | -------------------------------------------------------------------------------- /css/tab.css: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2022-2024 Jankari Tech Pvt. Ltd. 3 | * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors 4 | * SPDX-License-Identifier: AGPL-3.0-or-later 5 | */ 6 | 7 | @import '../css/dashboard.css'; 8 | 9 | #tab-open-project { 10 | height: 100%; 11 | padding: 0; 12 | } 13 | 14 | .loading-spinner { 15 | animation-name: spin; 16 | animation-duration: 1000ms; 17 | animation-iteration-count: infinite; 18 | animation-timing-function: linear; 19 | } 20 | 21 | @keyframes spin { 22 | from {transform: rotate(0deg);} 23 | to {transform: rotate(360deg);} 24 | } 25 | -------------------------------------------------------------------------------- /dev/.env: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Jankari Tech Pvt. Ltd. 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | # 4 | # Nextcloud configurations 5 | NEXTCLOUD_IMAGE_TAG= 6 | # e.g.: 83, 82 7 | NEXTCLOUD_IMAGE_PHP= 8 | NEXTCLOUD_BRANCH= 9 | NEXTCLOUD_AUTOINSTALL_APPS= 10 | 11 | # OpenProject configurations 12 | OPENPROJECT_IMAGE_TAG= 13 | OPENPROJECT_DEV_HOST= 14 | OPENPROJECT_RAILS__RELATIVE__URL__ROOT= 15 | OPENPROJECT_EDITION= 16 | 17 | # run keycloak 18 | # KEYCLOAK=:keycloak.yaml 19 | 20 | COMPOSE_FILE=compose.yaml${KEYCLOAK:-} 21 | -------------------------------------------------------------------------------- /dev/apps/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextcloud/integration_openproject/be3412e2fae7bf9afe127358f3e6fcd3a478040f/dev/apps/.keep -------------------------------------------------------------------------------- /dev/keycloak.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Jankari Tech Pvt. Ltd. 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | services: 4 | keycloak-db: 5 | image: postgres:13 6 | restart: always 7 | networks: 8 | - nc-op 9 | environment: 10 | POSTGRES_DB: keycloak 11 | POSTGRES_USER: keycloak 12 | POSTGRES_PASSWORD: keycloak 13 | 14 | keycloak: 15 | image: quay.io/keycloak/keycloak:21.1 16 | restart: always 17 | command: 18 | [ 19 | 'start-dev', 20 | '--proxy edge', 21 | '--spi-connections-http-client-default-disable-trust-manager=true', 22 | '--import-realm' 23 | ] 24 | environment: 25 | KC_DB: postgres 26 | KC_DB_USERNAME: keycloak 27 | KC_DB_PASSWORD: keycloak 28 | KC_DB_URL: jdbc:postgresql://keycloak-db:5432/keycloak 29 | KEYCLOAK_ADMIN: admin 30 | KEYCLOAK_ADMIN_PASSWORD: admin 31 | KC_DB_SCHEMA: public 32 | KC_HOSTNAME: keycloak.local 33 | KC_FEATURES: preview 34 | KC_TRANSACTION_XA_ENABLED: false 35 | networks: 36 | - nc-op 37 | volumes: 38 | - ./opnc-realm.json:/opt/keycloak/data/import/opnc-realm.json 39 | - step:/step:ro 40 | - keycloakdata:/opt/keycloak/data/ 41 | labels: 42 | traefik.enable: true 43 | traefik.http.routers.keycloak.rule: Host(`keycloak.local`) 44 | traefik.http.routers.keycloak.entrypoints: websecure 45 | depends_on: 46 | - keycloak-db 47 | - traefik 48 | 49 | volumes: 50 | keycloakdata: 51 | -------------------------------------------------------------------------------- /dev/nextcloud.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # SPDX-FileCopyrightText: 2025 Jankari Tech Pvt. Ltd. 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | set -e 6 | 7 | rm -rf /tmp/server || true 8 | # clone nextcloud server 9 | git clone -b "${SERVER_BRANCH}" --depth 1 https://github.com/nextcloud/server.git /tmp/server 10 | 11 | (cd /tmp/server && git submodule update --init) 12 | rsync -a --chmod=755 --chown=www-data:www-data /tmp/server/ /var/www/html 13 | chown www-data: -R /var/www/html/data 14 | chown www-data: /var/www/html/.htaccess 15 | 16 | # run the nextcloud setup 17 | /usr/local/bin/bootstrap.sh apache2-foreground 18 | -------------------------------------------------------------------------------- /dev/nextcloud/config.php: -------------------------------------------------------------------------------- 1 | 10 | array( 11 | 0 => 12 | array( 13 | 'path' => '/var/www/html/apps', 14 | 'url' => '/apps', 15 | 'writable' => false, 16 | ), 17 | 1 => 18 | array( 19 | 'path' => '/var/www/html/custom_apps', 20 | 'url' => '/custom_apps', 21 | 'writable' => true, 22 | ), 23 | ), 24 | ]; 25 | -------------------------------------------------------------------------------- /dev/nextcloud/hooks/after-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # SPDX-FileCopyrightText: 2025 Jankari Tech Pvt. Ltd. 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | set -e 6 | 7 | OCC() { 8 | # shellcheck disable=SC2068 9 | sudo -E -u www-data php "$WEBROOT/occ" $@ 10 | } 11 | 12 | OCC a:e integration_openproject 13 | OCC security:certificates:import /etc/ssl/certs/ca-certificates.crt 14 | -------------------------------------------------------------------------------- /dev/nextcloud/hooks/before-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # SPDX-FileCopyrightText: 2025 Jankari Tech Pvt. Ltd. 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | set -e 6 | 7 | echo "-----------------------------------------------------------" 8 | echo "[INFO] Installing CA certificates..." 9 | echo "-----------------------------------------------------------" 10 | 11 | STEP_CERTS_DIR="/step/certs" 12 | 13 | if [ -d "$STEP_CERTS_DIR" ]; then 14 | rm -rf /etc/ssl/certs/Step_Root_CA.pem /usr/local/share/ca-certificates/Step_Root_CA.crt 15 | echo "[INFO] Linking root CA certificate..." 16 | cp "$STEP_CERTS_DIR"/root_ca.crt /usr/local/share/ca-certificates/Step_Root_CA.crt 17 | update-ca-certificates 18 | fi 19 | 20 | # fix custom_apps permissions 21 | chown www-data custom_apps 22 | find ./custom_apps -mindepth 1 -path ./custom_apps/integration_openproject -prune -o -exec chown www-data {} \; 23 | -------------------------------------------------------------------------------- /dev/openproject.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # SPDX-FileCopyrightText: 2025 Jankari Tech Pvt. Ltd. 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | STEP_CERTS_DIR="/step/certs" 6 | 7 | if [ -d "$STEP_CERTS_DIR" ]; then 8 | rm -rf /etc/ssl/certs/Step_Root_CA.pem /usr/local/share/ca-certificates/Step_Root_CA.crt 9 | echo "[INFO] Linking root CA certificate..." 10 | cp "$STEP_CERTS_DIR"/root_ca.crt /usr/local/share/ca-certificates/Step_Root_CA.crt 11 | update-ca-certificates 12 | fi 13 | 14 | ./docker/prod/entrypoint.sh ./docker/prod/supervisord 15 | -------------------------------------------------------------------------------- /dev/opnc-realm.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2025 Jankari Tech Pvt. Ltd. 2 | SPDX-License-Identifier: AGPL-3.0-or-later 3 | -------------------------------------------------------------------------------- /dev/ssl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # SPDX-FileCopyrightText: 2025 Jankari Tech Pvt. Ltd. 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | tmp_cert_dir="$HOME/tmp" 6 | 7 | mkdir -p "$tmp_cert_dir" 8 | 9 | tmp_cert="$tmp_cert_dir/root_ca.crt" 10 | 11 | sudo rm -rf "$tmp_cert" /usr/local/share/ca-certificates/Step_Root_CA.crt /etc/ssl/certs/Step_Root_CA.pem 12 | 13 | docker compose cp step:/home/step/certs/root_ca.crt "$tmp_cert" 14 | sudo cp "$tmp_cert" /usr/local/share/ca-certificates/Step_Root_CA.crt 15 | sudo update-ca-certificates 16 | 17 | cert_db="$HOME/.pki/nssdb" 18 | # delete existing cert 19 | certutil -D -n "NC-OP Integration Root CA" -d sql:"$cert_db" 20 | # add root CA to cert db 21 | certutil -A -n "NC-OP Integration Root CA" -t TC -d sql:"$cert_db" -i "$tmp_cert" 22 | # update/rebuild cert db 23 | certutil -M -d sql:"$cert_db" 24 | # list certs 25 | certutil -L -d sql:"$cert_db" 26 | -------------------------------------------------------------------------------- /dev/step.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # SPDX-FileCopyrightText: 2025 Jankari Tech Pvt. Ltd. 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | 5 | rm -f /certs/acme.json 6 | touch /certs/acme.json 7 | chmod 600 /certs/acme.json 8 | 9 | bash /entrypoint.sh 10 | 11 | HOME=/home/step 12 | 13 | # update the certificate duration to 1 year 14 | step ca provisioner update acme --x509-min-dur=24h --x509-max-dur=8760h --x509-default-dur=8760h 15 | 16 | cp "$HOME/certs/root_ca.crt" "$HOME/certs/Step_Root_CA.crt" 17 | ln -s "$HOME/certs/Step_Root_CA.crt" /etc/ssl/certs/Step_Root_CA.pem 18 | update-ca-certificates 19 | 20 | step-ca --password-file $PWDPATH $CONFIGPATH 21 | -------------------------------------------------------------------------------- /docker-compose.override.example.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 OpenProject GmbH 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | services: 4 | nextcloud: 5 | ports: 6 | - "8080:80" 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022-2023 Jankari Tech Pvt. Ltd. 2 | # SPDX-FileCopyrightText: 2022 OpenProject GmbH 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | version: '3.9' 5 | 6 | services: 7 | db: 8 | image: postgres:14 9 | restart: always 10 | volumes: 11 | - db:/var/lib/postgresql/data 12 | networks: 13 | - internal 14 | environment: 15 | - POSTGRES_DB=nextcloud 16 | - POSTGRES_USER=nextcloud 17 | - POSTGRES_PASSWORD=nextcloud 18 | 19 | nextcloud: 20 | image: nextcloud:apache 21 | restart: always 22 | extra_hosts: 23 | - "host.docker.internal:host-gateway" 24 | volumes: 25 | - nextcloud:/var/www/html 26 | - apache-config:/etc/apache2 27 | - ${APP_DIR:-./../../custom_apps}:/var/www/html/custom_apps 28 | networks: 29 | - internal 30 | environment: 31 | - POSTGRES_HOST=db 32 | - POSTGRES_DB=nextcloud 33 | - POSTGRES_USER=nextcloud 34 | - POSTGRES_PASSWORD=nextcloud 35 | depends_on: 36 | - db 37 | 38 | cron: 39 | image: nextcloud:apache 40 | restart: always 41 | volumes: 42 | - nextcloud:/var/www/html 43 | - apache-config:/etc/apache2 44 | networks: 45 | - internal 46 | entrypoint: /cron.sh 47 | depends_on: 48 | - db 49 | 50 | volumes: 51 | db: 52 | nextcloud: 53 | apache-config: 54 | 55 | networks: 56 | internal: 57 | -------------------------------------------------------------------------------- /docs/release.md: -------------------------------------------------------------------------------- 1 | 5 | # Release Process 6 | 7 | ## 1. Release Preparation 8 | 9 | ### a. Major/Minor Release 10 | 11 | 1. Create a release branch from the master with the format `release/.` (e.g. `release/2.1`). 12 | 13 | On the release branch: 14 | 15 | 2. In case any new feature was added, update the feature description in: 16 | - `README.md` 17 | - `appinfo/info.xml` 18 | 3. Update the version in: 19 | - `appinfo/info.xml` 20 | - `package.json` 21 | 4. Add the new release branch in `.tx/backport` to allow transifex commits. 22 | 5. Update `CHANGELOG.md` with the changes and the version to be released. 23 | 6. Update the new release branch in the [nightly CI](../.github/workflows/nighlty-ci-release-branch.yml). 24 | 7. Perform confirmatory testing (Changelogs) - by the OpenProject team. 25 | 8. Perform [smoke testing](testing/smoke_testing.md) - by the OpenProject team. 26 | 27 | ### b. Patch Release 28 | 29 | On the current release branch: 30 | 31 | 1. Update the patch version in: 32 | - `appinfo/info.xml` 33 | - `package.json` 34 | 2. Update `CHANGELOG.md` with the changes and the version to be released. 35 | 3. Perform confirmatory testing (Changelogs) - by the OpenProject team. 36 | 4. Perform [smoke testing](testing/smoke_testing.md) - by the OpenProject team. 37 | 38 | ## 2. Publish Release 39 | 40 | > [!IMPORTANT] 41 | > 42 | > The tag MUST follow the following format: 43 | > 44 | > - For release: `vX.Y.Z` (e.g. `v2.1.1`) 45 | > - For test release: `vX.Y.Z-yyyymmdd-nightly` (e.g. `v2.1.1-20220928-nightly`) 46 | 47 | 1. Tag a commit from the release branch. 48 | 49 | ```bash 50 | git tag vX.Y.Z -m "vX.Y.Z" 51 | 52 | # E.g.: 53 | # git tag v2.1.1-20220928-nightly -m "v2.1.1-20220928-nightly" 54 | ``` 55 | 56 | > **_NOTE:_** Every tag should be created with a unique commit, or the publish will fail. 57 | 58 | 2. Push the tag to the `auto-release` branch. 59 | 60 | ```bash 61 | git push origin release/.:auto-release vX.Y.Z 62 | 63 | # E.g.: 64 | # git push origin release/2.1:auto-release v2.1.1-20220928-nightly 65 | ``` 66 | 67 | 3. Approve the deployment in GitHub actions. 68 | 4. Check the release on Nextcloud [app store](https://apps.nextcloud.com/apps/integration_openproject/releases). 69 | 70 | ## 3. After Release 71 | 72 | 1. Add the release notes to the newly created [GitHub release](https://github.com/nextcloud/integration_openproject/releases). 73 | 2. Merge the necessary commits from the release branch into the `master` branch. 74 | 3. In the `master` branch, bump the app version to the next version (e.g.: `X.(Y+1).0-alpha.1`). 75 | -------------------------------------------------------------------------------- /docs/running_API_test.md: -------------------------------------------------------------------------------- 1 | 5 | ### 🧪 Running API tests 6 | 7 | > **_NOTE:_** 8 | > Before running the API tests, the nextcloud instance needs to be ready, and also integration app needs to be enabled 9 | 10 | To run the whole of the acceptance tests locally run the command below. 11 | ```shell 12 | NEXTCLOUD_BASE_URL=http:// make api-test 13 | ``` 14 | 15 | In order to run only a specific scenario 16 | ```shell 17 | NEXTCLOUD_BASE_URL=http:// \ 18 | make api-test \ 19 | FEATURE_PATH=tests/acceptance/features/api/directUpload.feature:15 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/setting_up_via_shell_script.md: -------------------------------------------------------------------------------- 1 | 5 | ## Setting up the integration with shell script 6 | 7 | `integration_setup.sh` sets up the whole integration with just one command. 8 | 9 | Prerequisites needed for using the shell script. 10 | 1. "OpenProject" is set up and running 11 | 2. "Nextcloud" is set up and running 12 | 3. The "OpenProject Integration" app is installed and enabled in Nextcloud. 13 | 4. The credentials of the OpenProject global user are known 14 | 5. In "Nextcloud" we already have set up an admin with credentials. 15 | 16 | Once all the above pre-conditions are met we can run the shell script to integrate with the following command. 17 | 18 | > Note: 19 | > - We can set the whole integration with or without `project folders` using this script with an environment variable `SETUP_PROJECT_FOLDER` 20 | > - `SETUP_PROJECT_FOLDER=true` will set the integration with `project folders` and vice-versa 21 | 22 | Also, the following bash command has an environment variable `OPENPROJECT_STORAGE_NAME` which will be the storage name to store the oauth information in Open Project required for integration. 23 | 24 | Below is an example of a command to run the script to set up the integration with `project folders` 25 | ```bash 26 | SETUP_PROJECT_FOLDER=true \ 27 | NEXTCLOUD_HOST= \ 28 | OPENPROJECT_HOST= \ 29 | OP_ADMIN_USERNAME= OP_ADMIN_PASSWORD= \ 30 | NC_ADMIN_USERNAME= NC_ADMIN_PASSWORD= \ 31 | OPENPROJECT_STORAGE_NAME= \ 32 | bash integration_setup.sh 33 | ``` 34 | 35 | > Note: these credentials are only used by the script to do the setup. They are not stored/remembered. 36 | -------------------------------------------------------------------------------- /img/app-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/app.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/bell-ring-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/bell-ring.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/screenshot1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextcloud/integration_openproject/be3412e2fae7bf9afe127358f3e6fcd3a478040f/img/screenshot1.jpg -------------------------------------------------------------------------------- /img/screenshot1.jpg.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2022 Jankari Tech Pvt. Ltd. 2 | SPDX-License-Identifier: AGPL-3.0-or-later 3 | -------------------------------------------------------------------------------- /img/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextcloud/integration_openproject/be3412e2fae7bf9afe127358f3e6fcd3a478040f/img/screenshot1.png -------------------------------------------------------------------------------- /img/screenshot1.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2023 OpenProject GmbH 2 | SPDX-License-Identifier: AGPL-3.0-or-later 3 | -------------------------------------------------------------------------------- /img/screenshot2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextcloud/integration_openproject/be3412e2fae7bf9afe127358f3e6fcd3a478040f/img/screenshot2.jpg -------------------------------------------------------------------------------- /img/screenshot2.jpg.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2022 Jankari Tech Pvt. Ltd. 2 | SPDX-License-Identifier: AGPL-3.0-or-later 3 | -------------------------------------------------------------------------------- /img/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextcloud/integration_openproject/be3412e2fae7bf9afe127358f3e6fcd3a478040f/img/screenshot2.png -------------------------------------------------------------------------------- /img/screenshot2.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2022 Jankari Tech Pvt. Ltd. 2 | SPDX-License-Identifier: AGPL-3.0-or-later 3 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2021-2024 Jankari Tech Pvt. Ltd. 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | const path = require('path') 7 | const rootDir = path.resolve(__dirname, '../../../') 8 | 9 | module.exports = { 10 | testMatch: ['**/tests/**/*.spec.{js,ts}'], 11 | moduleNameMapper: { 12 | '\\.(scss)$': '/tests/jest/stubs/empty.js', 13 | '@nextcloud/l10n/gettext': require.resolve('@nextcloud/l10n/gettext'), 14 | }, 15 | transform: { 16 | // process *.vue files with vue-jest 17 | '\\.vue$': '@vue/vue2-jest', 18 | '.+\\.(css|styl|less|sass|scss|jpg|jpeg|png|svg|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|avif)$': 19 | 'jest-transform-stub', 20 | '\\.c?js$': 'babel-jest', 21 | }, 22 | testEnvironment: 'jest-environment-jsdom', 23 | collectCoverage: true, 24 | coverageProvider: 'v8', 25 | collectCoverageFrom: ['./src/**'], 26 | coverageDirectory: '/coverage/jest/', 27 | coverageReporters: ['lcov', 'html', 'text'], 28 | transformIgnorePatterns: ['node_modules/(?!@ckeditor)/.+\\.js$'], 29 | setupFiles: ['/tests/jest/global.mock.js'], 30 | } 31 | -------------------------------------------------------------------------------- /l10n/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextcloud/integration_openproject/be3412e2fae7bf9afe127358f3e6fcd3a478040f/l10n/.gitkeep -------------------------------------------------------------------------------- /l10n/ast.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Cancel" : "Encaboxar", 5 | "Save" : "Guardar", 6 | "Invalid token" : "El pase ye inválidu", 7 | "Failed to save OpenProject options" : "Nun se puen guardar les opciones d'OpenProject", 8 | "URL is invalid" : "La URL ye inválida", 9 | "Response:" : "Rempuesta:", 10 | "Documentation" : "Documentación", 11 | "Copied!" : "¡Copióse!", 12 | "Copied to the clipboard" : "Copióse nel cartafueyu", 13 | "Details" : "Detalles", 14 | "Start typing to search" : "Comienza a escribir pa buscar", 15 | "Description" : "Descripción", 16 | "Create" : "Crear", 17 | "Failed to get OpenProject notifications" : "Nun se puen consiguir los avisos d'OpenProject", 18 | "Reset" : "Reafitar", 19 | "Invalid key" : "La clave ye inválida" 20 | }, 21 | "nplurals=2; plural=(n != 1);"); 22 | -------------------------------------------------------------------------------- /l10n/ast.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Cancel" : "Encaboxar", 3 | "Save" : "Guardar", 4 | "Invalid token" : "El pase ye inválidu", 5 | "Failed to save OpenProject options" : "Nun se puen guardar les opciones d'OpenProject", 6 | "URL is invalid" : "La URL ye inválida", 7 | "Response:" : "Rempuesta:", 8 | "Documentation" : "Documentación", 9 | "Copied!" : "¡Copióse!", 10 | "Copied to the clipboard" : "Copióse nel cartafueyu", 11 | "Details" : "Detalles", 12 | "Start typing to search" : "Comienza a escribir pa buscar", 13 | "Description" : "Descripción", 14 | "Create" : "Crear", 15 | "Failed to get OpenProject notifications" : "Nun se puen consiguir los avisos d'OpenProject", 16 | "Reset" : "Reafitar", 17 | "Invalid key" : "La clave ye inválida" 18 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 19 | } -------------------------------------------------------------------------------- /l10n/da.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Error getting OAuth access token" : "Fejl ved anmodning om OAuth adgangsnøgle", 5 | "Error during OAuth exchanges" : "Fejl under OAuth-udvekslinger", 6 | "Bad HTTP method" : "Dårlig HTTP-metode", 7 | "OAuth access token refused" : "OAuth adgangsnøgle afvist", 8 | "Cancel" : "Annuller", 9 | "Authentication method" : "Godkendelsesmetode", 10 | "Save" : "Gem", 11 | "Invalid token" : "Ugyldigt token ", 12 | "Connected as {user}" : "Forbundet som {user}", 13 | "Enable navigation link" : "Aktiver navigationslink", 14 | "Documentation" : "Dokumentation", 15 | "Copied!" : "Kopieret!", 16 | "Copied to the clipboard" : "Kopieret til udklipsholderen", 17 | "Details" : "Detaljer", 18 | "Start typing to search" : "Start med at skrive for at søge", 19 | "Select a user or group" : "Vælg en bruger eller gruppe", 20 | "Description" : "Beskrivelse", 21 | "Create" : "Opret", 22 | "Mark as read" : "Marker som læst", 23 | "Download and enable it" : "Download og aktiver det", 24 | "OAuth access token could not be obtained:" : "OAuth adgangsnøgle kunne ikke skaffes:", 25 | "Connected accounts" : "Forbundne konti", 26 | "Reset" : "Nulstil" 27 | }, 28 | "nplurals=2; plural=(n != 1);"); 29 | -------------------------------------------------------------------------------- /l10n/da.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Error getting OAuth access token" : "Fejl ved anmodning om OAuth adgangsnøgle", 3 | "Error during OAuth exchanges" : "Fejl under OAuth-udvekslinger", 4 | "Bad HTTP method" : "Dårlig HTTP-metode", 5 | "OAuth access token refused" : "OAuth adgangsnøgle afvist", 6 | "Cancel" : "Annuller", 7 | "Authentication method" : "Godkendelsesmetode", 8 | "Save" : "Gem", 9 | "Invalid token" : "Ugyldigt token ", 10 | "Connected as {user}" : "Forbundet som {user}", 11 | "Enable navigation link" : "Aktiver navigationslink", 12 | "Documentation" : "Dokumentation", 13 | "Copied!" : "Kopieret!", 14 | "Copied to the clipboard" : "Kopieret til udklipsholderen", 15 | "Details" : "Detaljer", 16 | "Start typing to search" : "Start med at skrive for at søge", 17 | "Select a user or group" : "Vælg en bruger eller gruppe", 18 | "Description" : "Beskrivelse", 19 | "Create" : "Opret", 20 | "Mark as read" : "Marker som læst", 21 | "Download and enable it" : "Download og aktiver det", 22 | "OAuth access token could not be obtained:" : "OAuth adgangsnøgle kunne ikke skaffes:", 23 | "Connected accounts" : "Forbundne konti", 24 | "Reset" : "Nulstil" 25 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 26 | } -------------------------------------------------------------------------------- /l10n/es_MX.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Cancel" : "Cancelar", 5 | "Authentication method" : "Método de autenticación", 6 | "Save" : "Guardar", 7 | "Documentation" : "Documentación", 8 | "Copied!" : "¡Copiado!", 9 | "Copied to the clipboard" : "Copiado al portapapeles", 10 | "Details" : "Detalles", 11 | "Start typing to search" : "Empiece a escribir para buscar", 12 | "Select a user or group" : "Seleccionar un usuario o grupo", 13 | "Description" : "Descripción", 14 | "Create" : "Crear", 15 | "Reset" : "Reiniciar" 16 | }, 17 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 18 | -------------------------------------------------------------------------------- /l10n/es_MX.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Cancel" : "Cancelar", 3 | "Authentication method" : "Método de autenticación", 4 | "Save" : "Guardar", 5 | "Documentation" : "Documentación", 6 | "Copied!" : "¡Copiado!", 7 | "Copied to the clipboard" : "Copiado al portapapeles", 8 | "Details" : "Detalles", 9 | "Start typing to search" : "Empiece a escribir para buscar", 10 | "Select a user or group" : "Seleccionar un usuario o grupo", 11 | "Description" : "Descripción", 12 | "Create" : "Crear", 13 | "Reset" : "Reiniciar" 14 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 15 | } -------------------------------------------------------------------------------- /l10n/et_EE.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "OpenProject" : "OpenProject", 5 | "Bad HTTP method" : "Vigane HTTP-meetod", 6 | "Cancel" : "Loobu", 7 | "Authentication method" : "Autentimise meetod", 8 | "Save" : "Salvesta", 9 | "Connected as {user}" : "Ühendatud kui {user}", 10 | "Documentation" : "Dokumentatsioon", 11 | "Copied!" : "Kopeeritud!", 12 | "Copied to the clipboard" : "Kopeeritud lõikelauale.", 13 | "Details" : "Üksikasjad", 14 | "Start typing to search" : "Otsimiseks alusta kirjutamist", 15 | "Select a user or group" : "Vali kasutaja või grupp", 16 | "Description" : "Kirjeldus", 17 | "Create" : "Lisa", 18 | "Mark as read" : "Märgi loetuks", 19 | "Connected accounts" : "Ühendatud kasutajakontod", 20 | "Reset" : "Lähtesta" 21 | }, 22 | "nplurals=2; plural=(n != 1);"); 23 | -------------------------------------------------------------------------------- /l10n/et_EE.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "OpenProject" : "OpenProject", 3 | "Bad HTTP method" : "Vigane HTTP-meetod", 4 | "Cancel" : "Loobu", 5 | "Authentication method" : "Autentimise meetod", 6 | "Save" : "Salvesta", 7 | "Connected as {user}" : "Ühendatud kui {user}", 8 | "Documentation" : "Dokumentatsioon", 9 | "Copied!" : "Kopeeritud!", 10 | "Copied to the clipboard" : "Kopeeritud lõikelauale.", 11 | "Details" : "Üksikasjad", 12 | "Start typing to search" : "Otsimiseks alusta kirjutamist", 13 | "Select a user or group" : "Vali kasutaja või grupp", 14 | "Description" : "Kirjeldus", 15 | "Create" : "Lisa", 16 | "Mark as read" : "Märgi loetuks", 17 | "Connected accounts" : "Ühendatud kasutajakontod", 18 | "Reset" : "Lähtesta" 19 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 20 | } -------------------------------------------------------------------------------- /l10n/fi.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Error getting OAuth access token" : "Virhe OAuth-valtuutuksen hakemisessa", 5 | "Error during OAuth exchanges" : "Virhe OAuth-tunnistautumisessa", 6 | "Bad HTTP method" : "Virheellinen HTTP-metodi", 7 | "OAuth access token refused" : "OAuth-valtuutus hylätty", 8 | "Cancel" : "Peruuta", 9 | "Authentication method" : "Tunnistautumistapa", 10 | "Save" : "Tallenna", 11 | "Incorrect access token" : "Virheellinen käyttöpoletti", 12 | "Connected as {user}" : "Yhdistetty käyttäjänä {user}", 13 | "Enable navigation link" : "Näytä navigointipalkissa", 14 | "Documentation" : "Dokumentaatio", 15 | "Copied!" : "Kopioitu!", 16 | "Copied to the clipboard" : "Kopioitu leikepöydälle", 17 | "Details" : "Tiedot", 18 | "Start typing to search" : "Aloita kirjoittaminen hakeaksesi", 19 | "Select a user or group" : "Valitse käyttäjä tai ryhmä", 20 | "Description" : "Kuvaus", 21 | "Create" : "Luo", 22 | "Mark as read" : "Merkitse luetuksi", 23 | "Unlink" : "Poista linkitys", 24 | "Connected accounts" : "Yhdistetyt tilit", 25 | "Reset" : "Palauta" 26 | }, 27 | "nplurals=2; plural=(n != 1);"); 28 | -------------------------------------------------------------------------------- /l10n/fi.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Error getting OAuth access token" : "Virhe OAuth-valtuutuksen hakemisessa", 3 | "Error during OAuth exchanges" : "Virhe OAuth-tunnistautumisessa", 4 | "Bad HTTP method" : "Virheellinen HTTP-metodi", 5 | "OAuth access token refused" : "OAuth-valtuutus hylätty", 6 | "Cancel" : "Peruuta", 7 | "Authentication method" : "Tunnistautumistapa", 8 | "Save" : "Tallenna", 9 | "Incorrect access token" : "Virheellinen käyttöpoletti", 10 | "Connected as {user}" : "Yhdistetty käyttäjänä {user}", 11 | "Enable navigation link" : "Näytä navigointipalkissa", 12 | "Documentation" : "Dokumentaatio", 13 | "Copied!" : "Kopioitu!", 14 | "Copied to the clipboard" : "Kopioitu leikepöydälle", 15 | "Details" : "Tiedot", 16 | "Start typing to search" : "Aloita kirjoittaminen hakeaksesi", 17 | "Select a user or group" : "Valitse käyttäjä tai ryhmä", 18 | "Description" : "Kuvaus", 19 | "Create" : "Luo", 20 | "Mark as read" : "Merkitse luetuksi", 21 | "Unlink" : "Poista linkitys", 22 | "Connected accounts" : "Yhdistetyt tilit", 23 | "Reset" : "Palauta" 24 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 25 | } -------------------------------------------------------------------------------- /l10n/he.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Error getting OAuth access token" : "שגיאה בקבלת אסימון גישה עם OAuth", 5 | "Error during OAuth exchanges" : "שגיאה במהלך החלפות OAuth", 6 | "Bad HTTP method" : "שגיאה במתודת HTTP", 7 | "OAuth access token refused" : "אסימון הגישה ב־OAuth סורב", 8 | "Cancel" : "ביטול", 9 | "Authentication method" : "שיטת אימות", 10 | "Save" : "שמירה", 11 | "Incorrect access token" : "אסימון הגישה שגוי", 12 | "Enable navigation link" : "הפעלת קישור ניווט", 13 | "Documentation" : "תיעוד", 14 | "Copied!" : "הועתק!", 15 | "Details" : "פרטים", 16 | "Start typing to search" : "להתחלת החיפוש יש להקליד", 17 | "Select a user or group" : "נא לבחור משתמש או קבוצה", 18 | "Description" : "תיאור", 19 | "Mark as read" : "סימון כנקרא", 20 | "Connected accounts" : "חשבונות מקושרים", 21 | "Reset" : "איפוס" 22 | }, 23 | "nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;"); 24 | -------------------------------------------------------------------------------- /l10n/he.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Error getting OAuth access token" : "שגיאה בקבלת אסימון גישה עם OAuth", 3 | "Error during OAuth exchanges" : "שגיאה במהלך החלפות OAuth", 4 | "Bad HTTP method" : "שגיאה במתודת HTTP", 5 | "OAuth access token refused" : "אסימון הגישה ב־OAuth סורב", 6 | "Cancel" : "ביטול", 7 | "Authentication method" : "שיטת אימות", 8 | "Save" : "שמירה", 9 | "Incorrect access token" : "אסימון הגישה שגוי", 10 | "Enable navigation link" : "הפעלת קישור ניווט", 11 | "Documentation" : "תיעוד", 12 | "Copied!" : "הועתק!", 13 | "Details" : "פרטים", 14 | "Start typing to search" : "להתחלת החיפוש יש להקליד", 15 | "Select a user or group" : "נא לבחור משתמש או קבוצה", 16 | "Description" : "תיאור", 17 | "Mark as read" : "סימון כנקרא", 18 | "Connected accounts" : "חשבונות מקושרים", 19 | "Reset" : "איפוס" 20 | },"pluralForm" :"nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;" 21 | } -------------------------------------------------------------------------------- /l10n/hr.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Error getting OAuth access token" : "Pogreška pri dohvaćanju tokena za pristup OAuth", 5 | "Error during OAuth exchanges" : "Pogreška tijekom razmjene radi autentifikacije OAuth", 6 | "Bad HTTP method" : "Pogrešna metoda HTTP-a", 7 | "OAuth access token refused" : "Odbijen token za pristup OAuth", 8 | "Cancel" : "Odustani", 9 | "Authentication method" : "Način autentifikacije", 10 | "Save" : "Spremi", 11 | "Incorrect access token" : "Pogrešan token za pristup", 12 | "Connected as {user}" : "Povezan kao {user}", 13 | "Enable navigation link" : "Omogući navigacijsku poveznicu", 14 | "Enable unified search for tickets" : "Omogući objedinjeno pretraživanje prijava", 15 | "Documentation" : "Dokumentacija", 16 | "Copied!" : "Kopirano!", 17 | "Details" : "Pojedinosti", 18 | "Start typing to search" : "Počnite unositi tekst za pretraživanje", 19 | "Select a user or group" : "Odaberite korisnika ili grupu", 20 | "Description" : "Opis", 21 | "Create" : "Stvori", 22 | "Mark as read" : "Označi kao pročitano", 23 | "OAuth access token could not be obtained:" : "Nije moguće dohvatiti token za pristup OAuth:", 24 | "Connected accounts" : "Povezani računi", 25 | "Reset" : "Resetiraj" 26 | }, 27 | "nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;"); 28 | -------------------------------------------------------------------------------- /l10n/hr.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Error getting OAuth access token" : "Pogreška pri dohvaćanju tokena za pristup OAuth", 3 | "Error during OAuth exchanges" : "Pogreška tijekom razmjene radi autentifikacije OAuth", 4 | "Bad HTTP method" : "Pogrešna metoda HTTP-a", 5 | "OAuth access token refused" : "Odbijen token za pristup OAuth", 6 | "Cancel" : "Odustani", 7 | "Authentication method" : "Način autentifikacije", 8 | "Save" : "Spremi", 9 | "Incorrect access token" : "Pogrešan token za pristup", 10 | "Connected as {user}" : "Povezan kao {user}", 11 | "Enable navigation link" : "Omogući navigacijsku poveznicu", 12 | "Enable unified search for tickets" : "Omogući objedinjeno pretraživanje prijava", 13 | "Documentation" : "Dokumentacija", 14 | "Copied!" : "Kopirano!", 15 | "Details" : "Pojedinosti", 16 | "Start typing to search" : "Počnite unositi tekst za pretraživanje", 17 | "Select a user or group" : "Odaberite korisnika ili grupu", 18 | "Description" : "Opis", 19 | "Create" : "Stvori", 20 | "Mark as read" : "Označi kao pročitano", 21 | "OAuth access token could not be obtained:" : "Nije moguće dohvatiti token za pristup OAuth:", 22 | "Connected accounts" : "Povezani računi", 23 | "Reset" : "Resetiraj" 24 | },"pluralForm" :"nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;" 25 | } -------------------------------------------------------------------------------- /l10n/id.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Error getting OAuth access token" : "Terjadi kesalahan mendapatkan token akses OAuth", 5 | "Error during OAuth exchanges" : "Terjadi kesalahan saat penukaran OAuth", 6 | "Bad HTTP method" : "Metode HTTP tidak benar", 7 | "OAuth access token refused" : "Token akses OAuth ditolak", 8 | "Cancel" : "Membatalkan", 9 | "Authentication method" : "Metode otentikasi", 10 | "Save" : "Simpan", 11 | "Incorrect access token" : "Token akses tidak benar", 12 | "Connected as {user}" : "Terhubung sebagai {user}", 13 | "Enable navigation link" : "Aktifkan tautan navigasi", 14 | "Documentation" : "Dokumentasi", 15 | "Copied!" : "Tersalin!", 16 | "Details" : "Detail", 17 | "Start typing to search" : "Mulai mengetik untuk mencari", 18 | "Select a user or group" : "Pilih pengguna atau grup", 19 | "Description" : "Deskrisi", 20 | "Create" : "Buat", 21 | "Mark as read" : "tandai sudah dibaca", 22 | "Connected accounts" : "Akun terhubung", 23 | "Reset" : "Setel ulang" 24 | }, 25 | "nplurals=1; plural=0;"); 26 | -------------------------------------------------------------------------------- /l10n/id.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Error getting OAuth access token" : "Terjadi kesalahan mendapatkan token akses OAuth", 3 | "Error during OAuth exchanges" : "Terjadi kesalahan saat penukaran OAuth", 4 | "Bad HTTP method" : "Metode HTTP tidak benar", 5 | "OAuth access token refused" : "Token akses OAuth ditolak", 6 | "Cancel" : "Membatalkan", 7 | "Authentication method" : "Metode otentikasi", 8 | "Save" : "Simpan", 9 | "Incorrect access token" : "Token akses tidak benar", 10 | "Connected as {user}" : "Terhubung sebagai {user}", 11 | "Enable navigation link" : "Aktifkan tautan navigasi", 12 | "Documentation" : "Dokumentasi", 13 | "Copied!" : "Tersalin!", 14 | "Details" : "Detail", 15 | "Start typing to search" : "Mulai mengetik untuk mencari", 16 | "Select a user or group" : "Pilih pengguna atau grup", 17 | "Description" : "Deskrisi", 18 | "Create" : "Buat", 19 | "Mark as read" : "tandai sudah dibaca", 20 | "Connected accounts" : "Akun terhubung", 21 | "Reset" : "Setel ulang" 22 | },"pluralForm" :"nplurals=1; plural=0;" 23 | } -------------------------------------------------------------------------------- /l10n/is.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Error getting OAuth access token" : "Villa við að ná í OAuth-aðgangsteikn", 5 | "Error during OAuth exchanges" : "Villa í OAuth-samskiptum", 6 | "OAuth access token refused" : "OAuth-aðgangsteikni hafnað", 7 | "Cancel" : "Hætta við", 8 | "Authentication method" : "Auðkenningarmáti", 9 | "Save" : "Vista", 10 | "Incorrect access token" : "Rangt aðgangsteikn", 11 | "Connected as {user}" : "Tengt sem {user}", 12 | "Enable navigation link" : "Virkja flakktengil", 13 | "Documentation" : "Hjálparskjöl", 14 | "Copied!" : "Afritað!", 15 | "Copied to the clipboard" : "Afritað á klippispjaldið", 16 | "Details" : "Details", 17 | "Start typing to search" : "Skrifaðu hér til að leita", 18 | "Select a user or group" : "Veldu notanda eða hóp", 19 | "Description" : "Lýsing", 20 | "Create" : "Búa til", 21 | "Mark as read" : "Merkja sem lesið", 22 | "Unlink" : "Aftengja", 23 | "Connected accounts" : "Tengdir aðgangar", 24 | "Reset" : "Endurstilla" 25 | }, 26 | "nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);"); 27 | -------------------------------------------------------------------------------- /l10n/is.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Error getting OAuth access token" : "Villa við að ná í OAuth-aðgangsteikn", 3 | "Error during OAuth exchanges" : "Villa í OAuth-samskiptum", 4 | "OAuth access token refused" : "OAuth-aðgangsteikni hafnað", 5 | "Cancel" : "Hætta við", 6 | "Authentication method" : "Auðkenningarmáti", 7 | "Save" : "Vista", 8 | "Incorrect access token" : "Rangt aðgangsteikn", 9 | "Connected as {user}" : "Tengt sem {user}", 10 | "Enable navigation link" : "Virkja flakktengil", 11 | "Documentation" : "Hjálparskjöl", 12 | "Copied!" : "Afritað!", 13 | "Copied to the clipboard" : "Afritað á klippispjaldið", 14 | "Details" : "Details", 15 | "Start typing to search" : "Skrifaðu hér til að leita", 16 | "Select a user or group" : "Veldu notanda eða hóp", 17 | "Description" : "Lýsing", 18 | "Create" : "Búa til", 19 | "Mark as read" : "Merkja sem lesið", 20 | "Unlink" : "Aftengja", 21 | "Connected accounts" : "Tengdir aðgangar", 22 | "Reset" : "Endurstilla" 23 | },"pluralForm" :"nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);" 24 | } -------------------------------------------------------------------------------- /l10n/ja.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Error getting OAuth access token" : "OAuth アクセストークン取得時のエラー", 5 | "Error during OAuth exchanges" : "OAuth 通信中のエラー", 6 | "Bad HTTP method" : "不正なHTTPメソッド", 7 | "OAuth access token refused" : "OAuth アクセストークンが拒否されました", 8 | "Cancel" : "キャンセル", 9 | "Authentication method" : "認証方法", 10 | "Save" : "保存", 11 | "Incorrect access token" : "正しくないアクセストークン", 12 | "Connected as {user}" : "{user} に接続済み", 13 | "Enable navigation link" : "ナビゲーションリンクを有効化", 14 | "Documentation" : "ドキュメント", 15 | "Copied!" : "コピーしました!", 16 | "Copied to the clipboard" : "クリップボードにコピーされました", 17 | "Details" : "詳細", 18 | "Start typing to search" : "入力して検索を開始します", 19 | "Select a user or group" : "ユーザーまたはグループを選択", 20 | "Description" : "説明", 21 | "Create" : "作成", 22 | "Mark as read" : "既読にする", 23 | "Unlink" : "アンリンク", 24 | "Connected accounts" : "接続済みアカウント", 25 | "Reset" : "リセット" 26 | }, 27 | "nplurals=1; plural=0;"); 28 | -------------------------------------------------------------------------------- /l10n/ja.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Error getting OAuth access token" : "OAuth アクセストークン取得時のエラー", 3 | "Error during OAuth exchanges" : "OAuth 通信中のエラー", 4 | "Bad HTTP method" : "不正なHTTPメソッド", 5 | "OAuth access token refused" : "OAuth アクセストークンが拒否されました", 6 | "Cancel" : "キャンセル", 7 | "Authentication method" : "認証方法", 8 | "Save" : "保存", 9 | "Incorrect access token" : "正しくないアクセストークン", 10 | "Connected as {user}" : "{user} に接続済み", 11 | "Enable navigation link" : "ナビゲーションリンクを有効化", 12 | "Documentation" : "ドキュメント", 13 | "Copied!" : "コピーしました!", 14 | "Copied to the clipboard" : "クリップボードにコピーされました", 15 | "Details" : "詳細", 16 | "Start typing to search" : "入力して検索を開始します", 17 | "Select a user or group" : "ユーザーまたはグループを選択", 18 | "Description" : "説明", 19 | "Create" : "作成", 20 | "Mark as read" : "既読にする", 21 | "Unlink" : "アンリンク", 22 | "Connected accounts" : "接続済みアカウント", 23 | "Reset" : "リセット" 24 | },"pluralForm" :"nplurals=1; plural=0;" 25 | } -------------------------------------------------------------------------------- /l10n/ka.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Cancel" : "Cancel", 5 | "Save" : "Save", 6 | "Documentation" : "Documentation", 7 | "Copied!" : "Copied!", 8 | "Details" : "Details", 9 | "Start typing to search" : "Start typing to search", 10 | "Select a user or group" : "Select a user or group", 11 | "Description" : "Description", 12 | "Create" : "Create", 13 | "Mark as read" : "Mark as read", 14 | "Unlink" : "Unlink", 15 | "Reset" : "Reset" 16 | }, 17 | "nplurals=2; plural=(n!=1);"); 18 | -------------------------------------------------------------------------------- /l10n/ka.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Cancel" : "Cancel", 3 | "Save" : "Save", 4 | "Documentation" : "Documentation", 5 | "Copied!" : "Copied!", 6 | "Details" : "Details", 7 | "Start typing to search" : "Start typing to search", 8 | "Select a user or group" : "Select a user or group", 9 | "Description" : "Description", 10 | "Create" : "Create", 11 | "Mark as read" : "Mark as read", 12 | "Unlink" : "Unlink", 13 | "Reset" : "Reset" 14 | },"pluralForm" :"nplurals=2; plural=(n!=1);" 15 | } -------------------------------------------------------------------------------- /l10n/ko.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Error getting OAuth access token" : "OAuth 접근 토큰을 구하는 오류", 5 | "Error during OAuth exchanges" : "OAuth 교환 중 발생한 오류", 6 | "Bad HTTP method" : "옳지 않은 HTTP 메소드", 7 | "OAuth access token refused" : "OAuth 접근 토큰 거부됨", 8 | "Cancel" : "취소", 9 | "Authentication method" : "인증 방법", 10 | "Save" : "저장", 11 | "Incorrect access token" : "잘못된 액세스 토큰", 12 | "Invalid token" : "잘못된 토큰", 13 | "Connected as {user}" : "{user}로 연결됨", 14 | "Server replied with an error message, please check the Nextcloud logs" : "서버가 오류 메시지로 응답했습니다. Nextcloud 로그를 참조하십시오.", 15 | "Documentation" : "문서", 16 | "Copied!" : "복사 성공!", 17 | "Copied to the clipboard" : "클립보드로 복사됨", 18 | "Details" : "세부사항", 19 | "No OpenProject account connected" : "연결된 OpenProject 계정 없음", 20 | "Start typing to search" : "검색어 입력", 21 | "Select a user or group" : "사용자 또는 그룹 선택", 22 | "Description" : "설명", 23 | "Create" : "생성", 24 | "Mark as read" : "읽은 것으로 표시", 25 | "Unlink" : "링크 해제", 26 | "OAuth access token could not be obtained:" : "OAuth 접근 토큰을 얻을 수 없었습니다: ", 27 | "Connected accounts" : "계정 연결됨", 28 | "Reset" : "초기화" 29 | }, 30 | "nplurals=1; plural=0;"); 31 | -------------------------------------------------------------------------------- /l10n/ko.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Error getting OAuth access token" : "OAuth 접근 토큰을 구하는 오류", 3 | "Error during OAuth exchanges" : "OAuth 교환 중 발생한 오류", 4 | "Bad HTTP method" : "옳지 않은 HTTP 메소드", 5 | "OAuth access token refused" : "OAuth 접근 토큰 거부됨", 6 | "Cancel" : "취소", 7 | "Authentication method" : "인증 방법", 8 | "Save" : "저장", 9 | "Incorrect access token" : "잘못된 액세스 토큰", 10 | "Invalid token" : "잘못된 토큰", 11 | "Connected as {user}" : "{user}로 연결됨", 12 | "Server replied with an error message, please check the Nextcloud logs" : "서버가 오류 메시지로 응답했습니다. Nextcloud 로그를 참조하십시오.", 13 | "Documentation" : "문서", 14 | "Copied!" : "복사 성공!", 15 | "Copied to the clipboard" : "클립보드로 복사됨", 16 | "Details" : "세부사항", 17 | "No OpenProject account connected" : "연결된 OpenProject 계정 없음", 18 | "Start typing to search" : "검색어 입력", 19 | "Select a user or group" : "사용자 또는 그룹 선택", 20 | "Description" : "설명", 21 | "Create" : "생성", 22 | "Mark as read" : "읽은 것으로 표시", 23 | "Unlink" : "링크 해제", 24 | "OAuth access token could not be obtained:" : "OAuth 접근 토큰을 얻을 수 없었습니다: ", 25 | "Connected accounts" : "계정 연결됨", 26 | "Reset" : "초기화" 27 | },"pluralForm" :"nplurals=1; plural=0;" 28 | } -------------------------------------------------------------------------------- /l10n/lt_LT.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Error getting OAuth access token" : "Klaida gaunant „OAuth“ prieigos raktą", 5 | "Error during OAuth exchanges" : "Klaida „OAuth“ apsikeitimo metu", 6 | "Direct download error" : "Tiesioginio atsisiuntimo klaida", 7 | "Bad HTTP method" : "Blogas HTTP metodas", 8 | "OAuth access token refused" : "„OAuth“ prieigos raktas atmestas", 9 | "Yes, replace" : "Taip, pakeisti", 10 | "Cancel" : "Atsisakyti", 11 | "Authentication method" : "Tapatybės nustatymo metodas", 12 | "Save" : "Įrašyti", 13 | "Invalid token" : "Neteisingas prieigos raktas", 14 | "Connected as {user}" : "Prisijungta kaip {user}", 15 | "Documentation" : "Dokumentacija", 16 | "Copied!" : "Nukopijuota!", 17 | "Copy value" : "Kopijuoti reikšmę", 18 | "Copied to the clipboard" : "Nukopijuota į iškarpinę", 19 | "Details" : "Išsamiau", 20 | "Unexpected Error" : "Netikėta klaida", 21 | "Start typing to search" : "Rašykite norėdami atlikti paiešką", 22 | "Select a user or group" : "Pasirinkti naudotoją ar grupę", 23 | "Description" : "Aprašas", 24 | "Create" : "Sukurti", 25 | "Mark as read" : "Žymėti kaip skaitytą", 26 | "Unlink" : "Atsieti", 27 | "Connected accounts" : "Prijungtos paskyros", 28 | "Yes, reset" : "Taip, atstatyti", 29 | "Reset" : "Atstatyti" 30 | }, 31 | "nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);"); 32 | -------------------------------------------------------------------------------- /l10n/lt_LT.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Error getting OAuth access token" : "Klaida gaunant „OAuth“ prieigos raktą", 3 | "Error during OAuth exchanges" : "Klaida „OAuth“ apsikeitimo metu", 4 | "Direct download error" : "Tiesioginio atsisiuntimo klaida", 5 | "Bad HTTP method" : "Blogas HTTP metodas", 6 | "OAuth access token refused" : "„OAuth“ prieigos raktas atmestas", 7 | "Yes, replace" : "Taip, pakeisti", 8 | "Cancel" : "Atsisakyti", 9 | "Authentication method" : "Tapatybės nustatymo metodas", 10 | "Save" : "Įrašyti", 11 | "Invalid token" : "Neteisingas prieigos raktas", 12 | "Connected as {user}" : "Prisijungta kaip {user}", 13 | "Documentation" : "Dokumentacija", 14 | "Copied!" : "Nukopijuota!", 15 | "Copy value" : "Kopijuoti reikšmę", 16 | "Copied to the clipboard" : "Nukopijuota į iškarpinę", 17 | "Details" : "Išsamiau", 18 | "Unexpected Error" : "Netikėta klaida", 19 | "Start typing to search" : "Rašykite norėdami atlikti paiešką", 20 | "Select a user or group" : "Pasirinkti naudotoją ar grupę", 21 | "Description" : "Aprašas", 22 | "Create" : "Sukurti", 23 | "Mark as read" : "Žymėti kaip skaitytą", 24 | "Unlink" : "Atsieti", 25 | "Connected accounts" : "Prijungtos paskyros", 26 | "Yes, reset" : "Taip, atstatyti", 27 | "Reset" : "Atstatyti" 28 | },"pluralForm" :"nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);" 29 | } -------------------------------------------------------------------------------- /l10n/lv.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Bad HTTP method" : "Nederīgs HTTP pieprasījuma veids", 5 | "Cancel" : "Atcelt", 6 | "Authentication method" : "Autentificēšanās veids", 7 | "Save" : "Saglabāt", 8 | "Automatically managed folders" : "Automātiski pārvaldītas mapes", 9 | "Automatically managed folders:" : "Automātiski pārvaldītās mapes:", 10 | "Documentation" : "Dokumentācija", 11 | "Copied!" : "Nokopēts!", 12 | "Details" : "Detaļas", 13 | "No OpenProject account connected" : "Nav sasaistītu OpenProject kontu", 14 | "Start typing to search" : "Sākt rakstīt, lai meklētu", 15 | "Status is not set to one of the allowed values." : "Stāvoklis nav iestatīts ar vienu no atļautajām vērtībām.", 16 | "Description" : "Apraksts", 17 | "Connected accounts" : "Sasaistītie konti", 18 | "Are you sure that you want to reset this app and delete all settings and all connections of all Nextcloud users to OpenProject?" : "Vai tiešam atiestatīt šo lietotni un izdzēst visu Nextcloud lietotāju visus iestatījumus un savienojumus ar OpenProject?", 19 | "Reset" : "Atiestatīt" 20 | }, 21 | "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);"); 22 | -------------------------------------------------------------------------------- /l10n/lv.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Bad HTTP method" : "Nederīgs HTTP pieprasījuma veids", 3 | "Cancel" : "Atcelt", 4 | "Authentication method" : "Autentificēšanās veids", 5 | "Save" : "Saglabāt", 6 | "Automatically managed folders" : "Automātiski pārvaldītas mapes", 7 | "Automatically managed folders:" : "Automātiski pārvaldītās mapes:", 8 | "Documentation" : "Dokumentācija", 9 | "Copied!" : "Nokopēts!", 10 | "Details" : "Detaļas", 11 | "No OpenProject account connected" : "Nav sasaistītu OpenProject kontu", 12 | "Start typing to search" : "Sākt rakstīt, lai meklētu", 13 | "Status is not set to one of the allowed values." : "Stāvoklis nav iestatīts ar vienu no atļautajām vērtībām.", 14 | "Description" : "Apraksts", 15 | "Connected accounts" : "Sasaistītie konti", 16 | "Are you sure that you want to reset this app and delete all settings and all connections of all Nextcloud users to OpenProject?" : "Vai tiešam atiestatīt šo lietotni un izdzēst visu Nextcloud lietotāju visus iestatījumus un savienojumus ar OpenProject?", 17 | "Reset" : "Atiestatīt" 18 | },"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);" 19 | } -------------------------------------------------------------------------------- /l10n/mk.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Error getting OAuth access token" : "Грешка при добивање на OAuth пристапен токен", 5 | "Error during OAuth exchanges" : "Грешка при размена на податоци со OAuth ", 6 | "OAuth access token refused" : "Одбиен OAuth пристапен токен ", 7 | "Cancel" : "Откажи", 8 | "Authentication method" : "Начин на автентификација", 9 | "Save" : "Зачувај", 10 | "Incorrect access token" : "Неточен токен за пристап", 11 | "Connected as {user}" : "Поврзан како {user}", 12 | "Enable navigation link" : "Овозможи линк за навигација", 13 | "Documentation" : "Документација", 14 | "Copied!" : "Копирано!", 15 | "Details" : "Детали", 16 | "Start typing to search" : "Напишете нешто за пребарување", 17 | "Select a user or group" : "Избери корисник или група", 18 | "Create" : "Креирај", 19 | "Mark as read" : "Означи како прочитано", 20 | "Connected accounts" : "Поврзани сметки", 21 | "Reset" : "Ресетирање" 22 | }, 23 | "nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;"); 24 | -------------------------------------------------------------------------------- /l10n/mk.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Error getting OAuth access token" : "Грешка при добивање на OAuth пристапен токен", 3 | "Error during OAuth exchanges" : "Грешка при размена на податоци со OAuth ", 4 | "OAuth access token refused" : "Одбиен OAuth пристапен токен ", 5 | "Cancel" : "Откажи", 6 | "Authentication method" : "Начин на автентификација", 7 | "Save" : "Зачувај", 8 | "Incorrect access token" : "Неточен токен за пристап", 9 | "Connected as {user}" : "Поврзан како {user}", 10 | "Enable navigation link" : "Овозможи линк за навигација", 11 | "Documentation" : "Документација", 12 | "Copied!" : "Копирано!", 13 | "Details" : "Детали", 14 | "Start typing to search" : "Напишете нешто за пребарување", 15 | "Select a user or group" : "Избери корисник или група", 16 | "Create" : "Креирај", 17 | "Mark as read" : "Означи како прочитано", 18 | "Connected accounts" : "Поврзани сметки", 19 | "Reset" : "Ресетирање" 20 | },"pluralForm" :"nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;" 21 | } -------------------------------------------------------------------------------- /l10n/nl.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "OpenProject" : "OpenProject", 5 | "Error getting OAuth access token" : "Fout bij ophalen OAuth access token", 6 | "Error getting OAuth refresh token" : "Fout bij ophalen OAuth refresh token", 7 | "Error during OAuth exchanges" : "Fout tijdens OAuth uitwisselingen", 8 | "Direct download error" : "Onmiddelijke download fout", 9 | "Bad HTTP method" : "Foute HTTP methode", 10 | "OAuth access token refused" : "OAuth toegangstoken geweigerd", 11 | "OpenProject Integration" : "OpenProject integratie", 12 | "Administration > File storages" : "Beheer > Bestandsopslag", 13 | "Replace Nextcloud OAuth values" : "Vervang de NextCloud OAuth waarden", 14 | "Cancel" : "Annuleren", 15 | "Authentication method" : "Authenticatiemethode", 16 | "Save" : "Opslaan", 17 | "OpenProject OAuth settings" : "OpenProject OAuth instellingen", 18 | "Replace OpenProject OAuth values" : "Vervang de OpenProject OAuth waarden", 19 | "Nextcloud OAuth client" : "Nextcloud OAuth client", 20 | "Yes, I have copied these values" : "Ja, ik heb deze waarden gecopieerd", 21 | "Incorrect access token" : "Onjuist access token", 22 | "Invalid token" : "Ongeldig token", 23 | "Connected as {user}" : "Verbonden als {user}", 24 | "Enable navigation link" : "Inschakelen navigatielink", 25 | "Enable unified search for tickets" : "Inschakelen zoeken naar tickets", 26 | "Documentation" : "Documentatie", 27 | "OpenProject server" : "OpenProject server", 28 | "OpenProject host" : "OpenProject host", 29 | "Edit server information" : "server informatie aanpassen", 30 | "Copied!" : "Gekopieerd!", 31 | "Details" : "Details", 32 | "Unexpected Error" : "Onverwachte fout", 33 | "Start typing to search" : "Begin met typen om te zoeken", 34 | "Select a user or group" : "Kies een gebruiker of groep", 35 | "Description" : "Omschrijving", 36 | "Create" : "Creëer", 37 | "Mark as read" : "Markeren als gelezen", 38 | "Existing relations:" : "Bestaande relaties:", 39 | "OAuth access token could not be obtained:" : "OAuth access token kon niet worden opgehaald:", 40 | "OpenProject notifications" : "OpenProject meldingen", 41 | "OpenProject activity" : "OpenProject activiteit", 42 | "Connected accounts" : "Verbonden accounts", 43 | "Reset" : "Herstellen", 44 | "OpenProject integration" : "OpenProject integratie" 45 | }, 46 | "nplurals=2; plural=(n != 1);"); 47 | -------------------------------------------------------------------------------- /l10n/nl.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "OpenProject" : "OpenProject", 3 | "Error getting OAuth access token" : "Fout bij ophalen OAuth access token", 4 | "Error getting OAuth refresh token" : "Fout bij ophalen OAuth refresh token", 5 | "Error during OAuth exchanges" : "Fout tijdens OAuth uitwisselingen", 6 | "Direct download error" : "Onmiddelijke download fout", 7 | "Bad HTTP method" : "Foute HTTP methode", 8 | "OAuth access token refused" : "OAuth toegangstoken geweigerd", 9 | "OpenProject Integration" : "OpenProject integratie", 10 | "Administration > File storages" : "Beheer > Bestandsopslag", 11 | "Replace Nextcloud OAuth values" : "Vervang de NextCloud OAuth waarden", 12 | "Cancel" : "Annuleren", 13 | "Authentication method" : "Authenticatiemethode", 14 | "Save" : "Opslaan", 15 | "OpenProject OAuth settings" : "OpenProject OAuth instellingen", 16 | "Replace OpenProject OAuth values" : "Vervang de OpenProject OAuth waarden", 17 | "Nextcloud OAuth client" : "Nextcloud OAuth client", 18 | "Yes, I have copied these values" : "Ja, ik heb deze waarden gecopieerd", 19 | "Incorrect access token" : "Onjuist access token", 20 | "Invalid token" : "Ongeldig token", 21 | "Connected as {user}" : "Verbonden als {user}", 22 | "Enable navigation link" : "Inschakelen navigatielink", 23 | "Enable unified search for tickets" : "Inschakelen zoeken naar tickets", 24 | "Documentation" : "Documentatie", 25 | "OpenProject server" : "OpenProject server", 26 | "OpenProject host" : "OpenProject host", 27 | "Edit server information" : "server informatie aanpassen", 28 | "Copied!" : "Gekopieerd!", 29 | "Details" : "Details", 30 | "Unexpected Error" : "Onverwachte fout", 31 | "Start typing to search" : "Begin met typen om te zoeken", 32 | "Select a user or group" : "Kies een gebruiker of groep", 33 | "Description" : "Omschrijving", 34 | "Create" : "Creëer", 35 | "Mark as read" : "Markeren als gelezen", 36 | "Existing relations:" : "Bestaande relaties:", 37 | "OAuth access token could not be obtained:" : "OAuth access token kon niet worden opgehaald:", 38 | "OpenProject notifications" : "OpenProject meldingen", 39 | "OpenProject activity" : "OpenProject activiteit", 40 | "Connected accounts" : "Verbonden accounts", 41 | "Reset" : "Herstellen", 42 | "OpenProject integration" : "OpenProject integratie" 43 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 44 | } -------------------------------------------------------------------------------- /l10n/oc.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Cancel" : "Anullar", 5 | "Save" : "Enregistrar", 6 | "Invalid token" : "Geton invalid", 7 | "Connected as {user}" : "Connectat coma {user}", 8 | "Documentation" : "Documentacion", 9 | "Copied!" : "Copiat !", 10 | "Details" : "Detalhs", 11 | "Start typing to search" : "Començatz de picar per recercar", 12 | "Description" : "Descripcion", 13 | "Create" : "Crear", 14 | "Mark as read" : "Marcar coma legit", 15 | "Connected accounts" : "Comptes connectats", 16 | "Reset" : "Reïnicializar" 17 | }, 18 | "nplurals=2; plural=(n > 1);"); 19 | -------------------------------------------------------------------------------- /l10n/oc.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Cancel" : "Anullar", 3 | "Save" : "Enregistrar", 4 | "Invalid token" : "Geton invalid", 5 | "Connected as {user}" : "Connectat coma {user}", 6 | "Documentation" : "Documentacion", 7 | "Copied!" : "Copiat !", 8 | "Details" : "Detalhs", 9 | "Start typing to search" : "Començatz de picar per recercar", 10 | "Description" : "Descripcion", 11 | "Create" : "Crear", 12 | "Mark as read" : "Marcar coma legit", 13 | "Connected accounts" : "Comptes connectats", 14 | "Reset" : "Reïnicializar" 15 | },"pluralForm" :"nplurals=2; plural=(n > 1);" 16 | } -------------------------------------------------------------------------------- /l10n/pt_PT.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Error during OAuth exchanges" : "Erro durante trocas com o OAuth", 5 | "Bad HTTP method" : "Método HTTP incorreto", 6 | "Cancel" : "Cancelar", 7 | "Authentication method" : "Método de Autenticação", 8 | "Save" : "Guardar", 9 | "Invalid token" : "token inválido", 10 | "Documentation" : "Documentação", 11 | "Copied!" : "Copiado!", 12 | "Details" : "Detalhes", 13 | "Start typing to search" : "Digitar para pesquisar", 14 | "Select a user or group" : "Selecionar utilizador ou grupo", 15 | "Description" : "Descrição", 16 | "Create" : "Criar", 17 | "Reset" : "Reiniciar" 18 | }, 19 | "nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 20 | -------------------------------------------------------------------------------- /l10n/pt_PT.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Error during OAuth exchanges" : "Erro durante trocas com o OAuth", 3 | "Bad HTTP method" : "Método HTTP incorreto", 4 | "Cancel" : "Cancelar", 5 | "Authentication method" : "Método de Autenticação", 6 | "Save" : "Guardar", 7 | "Invalid token" : "token inválido", 8 | "Documentation" : "Documentação", 9 | "Copied!" : "Copiado!", 10 | "Details" : "Detalhes", 11 | "Start typing to search" : "Digitar para pesquisar", 12 | "Select a user or group" : "Selecionar utilizador ou grupo", 13 | "Description" : "Descrição", 14 | "Create" : "Criar", 15 | "Reset" : "Reiniciar" 16 | },"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 17 | } -------------------------------------------------------------------------------- /l10n/ro.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Error getting OAuth access token" : "Eroare în obținerea token-ului OAuth", 5 | "Error during OAuth exchanges" : "Eroare în schimbarea OAuth", 6 | "Bad HTTP method" : "Metodă HTTP nepotrivită", 7 | "OAuth access token refused" : "Token-ul OAuth a fost refuzat", 8 | "Cancel" : "Anulare", 9 | "Authentication method" : "Modul de autentificare", 10 | "Save" : "Salvează", 11 | "Enable navigation link" : "Pornește link-ul de navifare", 12 | "Documentation" : "Documentație", 13 | "Copied!" : "S-a copiat!", 14 | "Details" : "Detalii", 15 | "Start typing to search" : "Tastați pentru căutare", 16 | "Description" : "Descriere", 17 | "Create" : "Crează", 18 | "Mark as read" : "Marchează ca citit", 19 | "commented" : "comentat", 20 | "mentioned" : "menționat", 21 | "Unlink" : "Deconectare", 22 | "Connected accounts" : "Conturile conectate", 23 | "Reset" : "Resetare" 24 | }, 25 | "nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));"); 26 | -------------------------------------------------------------------------------- /l10n/ro.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Error getting OAuth access token" : "Eroare în obținerea token-ului OAuth", 3 | "Error during OAuth exchanges" : "Eroare în schimbarea OAuth", 4 | "Bad HTTP method" : "Metodă HTTP nepotrivită", 5 | "OAuth access token refused" : "Token-ul OAuth a fost refuzat", 6 | "Cancel" : "Anulare", 7 | "Authentication method" : "Modul de autentificare", 8 | "Save" : "Salvează", 9 | "Enable navigation link" : "Pornește link-ul de navifare", 10 | "Documentation" : "Documentație", 11 | "Copied!" : "S-a copiat!", 12 | "Details" : "Detalii", 13 | "Start typing to search" : "Tastați pentru căutare", 14 | "Description" : "Descriere", 15 | "Create" : "Crează", 16 | "Mark as read" : "Marchează ca citit", 17 | "commented" : "comentat", 18 | "mentioned" : "menționat", 19 | "Unlink" : "Deconectare", 20 | "Connected accounts" : "Conturile conectate", 21 | "Reset" : "Resetare" 22 | },"pluralForm" :"nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));" 23 | } -------------------------------------------------------------------------------- /l10n/sc.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Error getting OAuth access token" : "Errore recuperende su token de intrada OAuth.", 5 | "Error during OAuth exchanges" : "Errore cuncambiende OAuth", 6 | "Bad HTTP method" : "Mètodu HTTP no bàlidu", 7 | "OAuth access token refused" : "Token de intrada OAuth refudadu", 8 | "Cancel" : "Annulla", 9 | "Authentication method" : "Mètodu de autenticatzione", 10 | "Save" : "Sarva", 11 | "Incorrect access token" : "Token de intrada non bàlidu", 12 | "Connected as {user}" : "Connètidu comente {user}", 13 | "Enable navigation link" : "Ativa su ligòngiu de navigatzione", 14 | "Enable unified search for tickets" : "Ativa chirca unificada pro is tickets", 15 | "Documentation" : "Documentatzione", 16 | "Copied!" : "Copiados!", 17 | "Details" : "Detàllios", 18 | "Start typing to search" : "Cumintza a iscrìere pro chircare", 19 | "Select a user or group" : "Seletziona un'utente o grupu", 20 | "Description" : "Descritzione", 21 | "Create" : "Crea", 22 | "Mark as read" : "Marca comente lèghidu", 23 | "OAuth access token could not be obtained:" : "No at fatu a otènnere su token de intrada OAuth:", 24 | "Connected accounts" : "Contos connètidos", 25 | "Reset" : "Riprìstinu" 26 | }, 27 | "nplurals=2; plural=(n != 1);"); 28 | -------------------------------------------------------------------------------- /l10n/sc.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Error getting OAuth access token" : "Errore recuperende su token de intrada OAuth.", 3 | "Error during OAuth exchanges" : "Errore cuncambiende OAuth", 4 | "Bad HTTP method" : "Mètodu HTTP no bàlidu", 5 | "OAuth access token refused" : "Token de intrada OAuth refudadu", 6 | "Cancel" : "Annulla", 7 | "Authentication method" : "Mètodu de autenticatzione", 8 | "Save" : "Sarva", 9 | "Incorrect access token" : "Token de intrada non bàlidu", 10 | "Connected as {user}" : "Connètidu comente {user}", 11 | "Enable navigation link" : "Ativa su ligòngiu de navigatzione", 12 | "Enable unified search for tickets" : "Ativa chirca unificada pro is tickets", 13 | "Documentation" : "Documentatzione", 14 | "Copied!" : "Copiados!", 15 | "Details" : "Detàllios", 16 | "Start typing to search" : "Cumintza a iscrìere pro chircare", 17 | "Select a user or group" : "Seletziona un'utente o grupu", 18 | "Description" : "Descritzione", 19 | "Create" : "Crea", 20 | "Mark as read" : "Marca comente lèghidu", 21 | "OAuth access token could not be obtained:" : "No at fatu a otènnere su token de intrada OAuth:", 22 | "Connected accounts" : "Contos connètidos", 23 | "Reset" : "Riprìstinu" 24 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 25 | } -------------------------------------------------------------------------------- /l10n/sl.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Error getting OAuth access token" : "Napaka pri pridobivanju žetona OAuth za dostop", 5 | "Error during OAuth exchanges" : "Napaka med izmenjavo podatkov OAuth", 6 | "Bad HTTP method" : "Neustrezen način HTTP", 7 | "OAuth access token refused" : "Žeton OAuth za dostop je bil zavrnjen", 8 | "Cancel" : "Prekliči", 9 | "Authentication method" : "Način overitve", 10 | "Save" : "Shrani", 11 | "Incorrect access token" : "Neveljaven žeton za dostop", 12 | "Connected as {user}" : "Povezan je uporabniški račun {user}", 13 | "Enable navigation link" : "Omogoči povezave za krmarjenje", 14 | "Enable unified search for tickets" : "Omogoči enotno iskanje med objavami", 15 | "Documentation" : "Dokumentacija", 16 | "Copied!" : "Kopirano!", 17 | "Details" : "Podrobnosti", 18 | "Start typing to search" : "Vpišite niz za iskanje …", 19 | "Select a user or group" : "Izbor imena uporabnika oziroma skupine", 20 | "Description" : "Opis", 21 | "Create" : "Ustvari", 22 | "Mark as read" : "Označi kot prebrano", 23 | "Unlink" : "Odstrani povezavo", 24 | "OAuth access token could not be obtained:" : "Žetona OAuth za dostop ni mogoče pridobiti:", 25 | "Connected accounts" : "Povezani računi", 26 | "Reset" : "Ponastavi" 27 | }, 28 | "nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);"); 29 | -------------------------------------------------------------------------------- /l10n/sl.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Error getting OAuth access token" : "Napaka pri pridobivanju žetona OAuth za dostop", 3 | "Error during OAuth exchanges" : "Napaka med izmenjavo podatkov OAuth", 4 | "Bad HTTP method" : "Neustrezen način HTTP", 5 | "OAuth access token refused" : "Žeton OAuth za dostop je bil zavrnjen", 6 | "Cancel" : "Prekliči", 7 | "Authentication method" : "Način overitve", 8 | "Save" : "Shrani", 9 | "Incorrect access token" : "Neveljaven žeton za dostop", 10 | "Connected as {user}" : "Povezan je uporabniški račun {user}", 11 | "Enable navigation link" : "Omogoči povezave za krmarjenje", 12 | "Enable unified search for tickets" : "Omogoči enotno iskanje med objavami", 13 | "Documentation" : "Dokumentacija", 14 | "Copied!" : "Kopirano!", 15 | "Details" : "Podrobnosti", 16 | "Start typing to search" : "Vpišite niz za iskanje …", 17 | "Select a user or group" : "Izbor imena uporabnika oziroma skupine", 18 | "Description" : "Opis", 19 | "Create" : "Ustvari", 20 | "Mark as read" : "Označi kot prebrano", 21 | "Unlink" : "Odstrani povezavo", 22 | "OAuth access token could not be obtained:" : "Žetona OAuth za dostop ni mogoče pridobiti:", 23 | "Connected accounts" : "Povezani računi", 24 | "Reset" : "Ponastavi" 25 | },"pluralForm" :"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);" 26 | } -------------------------------------------------------------------------------- /l10n/sv.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Error getting OAuth access token" : "Kunde inte hämta OAuth-token", 5 | "Error during OAuth exchanges" : "Fel vid utväxling av OAuth-token", 6 | "Bad HTTP method" : "Felaktig HTTP-metod", 7 | "OAuth access token refused" : "OAuth-token avvisades", 8 | "Cancel" : "Avbryt", 9 | "Authentication method" : "Autentiseringsmetod", 10 | "Save" : "Spara", 11 | "Incorrect access token" : "Ogiltig åtkomst-token", 12 | "Invalid token" : "Ogiltig token", 13 | "Connected as {user}" : "Ansluten som {user}", 14 | "Enable navigation link" : "Aktivera navigeringslänk", 15 | "Enable unified search for tickets" : "Aktivera enhetlig sökning efter ärenden", 16 | "Warning, everything you type in the search bar will be sent to your OpenProject instance." : "Varning, allt du skriver i sökfältet kommer att skickas till din OpenProject-instans.", 17 | "Documentation" : "Dokumentation", 18 | "Copied!" : "Kopierad!", 19 | "Copied to the clipboard" : "Kopierat till urklipp", 20 | "Details" : "Detaljer", 21 | "Start typing to search" : "Börja skriva för att söka", 22 | "Select a user or group" : "Välj en användare eller grupp", 23 | "Description" : "Beskrivning", 24 | "Create" : "Skapa", 25 | "Mark as read" : "Markera som läst", 26 | "Unlink" : "Unlink", 27 | "OAuth access token could not be obtained:" : "OAuth-åtkomsttoken kunde inte erhållas:", 28 | "Connected accounts" : "Anslutna konton", 29 | "Reset" : "Återställ" 30 | }, 31 | "nplurals=2; plural=(n != 1);"); 32 | -------------------------------------------------------------------------------- /l10n/sv.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Error getting OAuth access token" : "Kunde inte hämta OAuth-token", 3 | "Error during OAuth exchanges" : "Fel vid utväxling av OAuth-token", 4 | "Bad HTTP method" : "Felaktig HTTP-metod", 5 | "OAuth access token refused" : "OAuth-token avvisades", 6 | "Cancel" : "Avbryt", 7 | "Authentication method" : "Autentiseringsmetod", 8 | "Save" : "Spara", 9 | "Incorrect access token" : "Ogiltig åtkomst-token", 10 | "Invalid token" : "Ogiltig token", 11 | "Connected as {user}" : "Ansluten som {user}", 12 | "Enable navigation link" : "Aktivera navigeringslänk", 13 | "Enable unified search for tickets" : "Aktivera enhetlig sökning efter ärenden", 14 | "Warning, everything you type in the search bar will be sent to your OpenProject instance." : "Varning, allt du skriver i sökfältet kommer att skickas till din OpenProject-instans.", 15 | "Documentation" : "Dokumentation", 16 | "Copied!" : "Kopierad!", 17 | "Copied to the clipboard" : "Kopierat till urklipp", 18 | "Details" : "Detaljer", 19 | "Start typing to search" : "Börja skriva för att söka", 20 | "Select a user or group" : "Välj en användare eller grupp", 21 | "Description" : "Beskrivning", 22 | "Create" : "Skapa", 23 | "Mark as read" : "Markera som läst", 24 | "Unlink" : "Unlink", 25 | "OAuth access token could not be obtained:" : "OAuth-åtkomsttoken kunde inte erhållas:", 26 | "Connected accounts" : "Anslutna konton", 27 | "Reset" : "Återställ" 28 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 29 | } -------------------------------------------------------------------------------- /l10n/th.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Cancel" : "ยกเลิก", 5 | "Authentication method" : "วิธีการตรวจสอบความถูกต้อง", 6 | "Save" : "บันทึก", 7 | "Connected as {user}" : "เชื่อมต่อเป็น {user} แล้ว", 8 | "Documentation" : "เอกสารประกอบ", 9 | "Copied!" : "คัดลอกแล้ว!", 10 | "Details" : "รายละเอียด", 11 | "Start typing to search" : "เริ่มพิมพ์เพื่อค้นหา", 12 | "Description" : "คำอธิบาย", 13 | "Create" : "สร้าง", 14 | "Reset" : "รีเซ็ต" 15 | }, 16 | "nplurals=1; plural=0;"); 17 | -------------------------------------------------------------------------------- /l10n/th.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Cancel" : "ยกเลิก", 3 | "Authentication method" : "วิธีการตรวจสอบความถูกต้อง", 4 | "Save" : "บันทึก", 5 | "Connected as {user}" : "เชื่อมต่อเป็น {user} แล้ว", 6 | "Documentation" : "เอกสารประกอบ", 7 | "Copied!" : "คัดลอกแล้ว!", 8 | "Details" : "รายละเอียด", 9 | "Start typing to search" : "เริ่มพิมพ์เพื่อค้นหา", 10 | "Description" : "คำอธิบาย", 11 | "Create" : "สร้าง", 12 | "Reset" : "รีเซ็ต" 13 | },"pluralForm" :"nplurals=1; plural=0;" 14 | } -------------------------------------------------------------------------------- /l10n/uk.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "OpenProject" : "OpenProject", 5 | "Error getting OAuth access token" : "Помилка отримання токен доступу OAuth", 6 | "Error getting OAuth refresh token" : "Помилка при отриманні токена доступу OAuth", 7 | "Error during OAuth exchanges" : "Помилка під час обміну OAuth", 8 | "Direct download error" : "Помилка прямого посилання", 9 | "This direct download link is invalid or has expired" : "Це пряме посилання для звантаження є недійсним або застарілим", 10 | "Bad HTTP method" : "Поганий метод HTTP", 11 | "OAuth access token refused" : "Токен доступу OAuth відхилено", 12 | "OpenProject Integration" : "Інтеграція OpenProject", 13 | "Link Nextcloud files to OpenProject work packages" : "Пов’яжіть файли Nextcloud із робочими пакетами OpenProject", 14 | "Replace Nextcloud OAuth values" : "Замінити значення Nextcloud OAuth", 15 | "Cancel" : "Скасувати", 16 | "Authentication method" : "Спосіб авторизації", 17 | "Save" : "Зберегти", 18 | "OpenProject OAuth settings" : "Параметри OpenProject OAuth", 19 | "Replace OpenProject OAuth values" : "Замінити значення OpenProject OAuth", 20 | "Nextcloud OAuth client" : "Клієнт Nextcloud OAuth", 21 | "Yes, I have copied these values" : "Так, я скопіював ці значення", 22 | "Create Nextcloud OAuth values" : "Створіть Nextcloud OAuth", 23 | "Project folders (recommended)" : "Каталоги проєкту (рекомендовано)", 24 | "Invalid token" : "Недісний токен", 25 | "Enable unified search for tickets" : "Увімкнути універсальний пошук заявок", 26 | "Documentation" : "Документація", 27 | "OpenProject server" : "Сервер OpenProject", 28 | "OpenProject host" : "Хост OpenProject", 29 | "Edit server information" : "Редагувати інформацію про сервер", 30 | "Copied!" : "Скопійовано!", 31 | "Copied to the clipboard" : "Скопійовано в буфер обміну", 32 | "Details" : "Деталі", 33 | "Unexpected Error" : "Неочікувана помилка", 34 | "Start typing to search" : "Що шукаємо?", 35 | "Select a user or group" : "Виберіть користувача або групу", 36 | "Description" : "Опис", 37 | "Create" : "Створити", 38 | "Mark as read" : "Позначити прочитаним", 39 | "OAuth access token could not be obtained:" : "Не вдалося отримати токен доступу OAuth:", 40 | "Connected accounts" : "Підключені облікові записи", 41 | "Please introduce your OpenProject host name" : "Будь ласка, введіть ім'я вашого хосту OpenProject", 42 | "Reset" : "Скидання" 43 | }, 44 | "nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);"); 45 | -------------------------------------------------------------------------------- /l10n/uk.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "OpenProject" : "OpenProject", 3 | "Error getting OAuth access token" : "Помилка отримання токен доступу OAuth", 4 | "Error getting OAuth refresh token" : "Помилка при отриманні токена доступу OAuth", 5 | "Error during OAuth exchanges" : "Помилка під час обміну OAuth", 6 | "Direct download error" : "Помилка прямого посилання", 7 | "This direct download link is invalid or has expired" : "Це пряме посилання для звантаження є недійсним або застарілим", 8 | "Bad HTTP method" : "Поганий метод HTTP", 9 | "OAuth access token refused" : "Токен доступу OAuth відхилено", 10 | "OpenProject Integration" : "Інтеграція OpenProject", 11 | "Link Nextcloud files to OpenProject work packages" : "Пов’яжіть файли Nextcloud із робочими пакетами OpenProject", 12 | "Replace Nextcloud OAuth values" : "Замінити значення Nextcloud OAuth", 13 | "Cancel" : "Скасувати", 14 | "Authentication method" : "Спосіб авторизації", 15 | "Save" : "Зберегти", 16 | "OpenProject OAuth settings" : "Параметри OpenProject OAuth", 17 | "Replace OpenProject OAuth values" : "Замінити значення OpenProject OAuth", 18 | "Nextcloud OAuth client" : "Клієнт Nextcloud OAuth", 19 | "Yes, I have copied these values" : "Так, я скопіював ці значення", 20 | "Create Nextcloud OAuth values" : "Створіть Nextcloud OAuth", 21 | "Project folders (recommended)" : "Каталоги проєкту (рекомендовано)", 22 | "Invalid token" : "Недісний токен", 23 | "Enable unified search for tickets" : "Увімкнути універсальний пошук заявок", 24 | "Documentation" : "Документація", 25 | "OpenProject server" : "Сервер OpenProject", 26 | "OpenProject host" : "Хост OpenProject", 27 | "Edit server information" : "Редагувати інформацію про сервер", 28 | "Copied!" : "Скопійовано!", 29 | "Copied to the clipboard" : "Скопійовано в буфер обміну", 30 | "Details" : "Деталі", 31 | "Unexpected Error" : "Неочікувана помилка", 32 | "Start typing to search" : "Що шукаємо?", 33 | "Select a user or group" : "Виберіть користувача або групу", 34 | "Description" : "Опис", 35 | "Create" : "Створити", 36 | "Mark as read" : "Позначити прочитаним", 37 | "OAuth access token could not be obtained:" : "Не вдалося отримати токен доступу OAuth:", 38 | "Connected accounts" : "Підключені облікові записи", 39 | "Please introduce your OpenProject host name" : "Будь ласка, введіть ім'я вашого хосту OpenProject", 40 | "Reset" : "Скидання" 41 | },"pluralForm" :"nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);" 42 | } -------------------------------------------------------------------------------- /l10n/vi.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Bad HTTP method" : "Phương thức HTTP không hợp lệ", 5 | "Cancel" : "Hủy", 6 | "Authentication method" : "Phương thức xác thực", 7 | "Save" : "Lưu", 8 | "Connected as {user}" : "Kết nối bởi {user}", 9 | "Documentation" : "Tài liệu", 10 | "Copied!" : "Đã sao chép!", 11 | "Details" : "Chi tiết", 12 | "Start typing to search" : "Nhập để tìm kiếm", 13 | "Description" : "Mô tả", 14 | "Create" : "‎Tạo‎", 15 | "Mark as read" : "Đánh dấu đã đọc", 16 | "Connected accounts" : "Đã kết nối tài khoản", 17 | "Reset" : "Đặt lại" 18 | }, 19 | "nplurals=1; plural=0;"); 20 | -------------------------------------------------------------------------------- /l10n/vi.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Bad HTTP method" : "Phương thức HTTP không hợp lệ", 3 | "Cancel" : "Hủy", 4 | "Authentication method" : "Phương thức xác thực", 5 | "Save" : "Lưu", 6 | "Connected as {user}" : "Kết nối bởi {user}", 7 | "Documentation" : "Tài liệu", 8 | "Copied!" : "Đã sao chép!", 9 | "Details" : "Chi tiết", 10 | "Start typing to search" : "Nhập để tìm kiếm", 11 | "Description" : "Mô tả", 12 | "Create" : "‎Tạo‎", 13 | "Mark as read" : "Đánh dấu đã đọc", 14 | "Connected accounts" : "Đã kết nối tài khoản", 15 | "Reset" : "Đặt lại" 16 | },"pluralForm" :"nplurals=1; plural=0;" 17 | } -------------------------------------------------------------------------------- /l10n/zh_CN.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "Error getting OAuth access token" : "获取 OAUth 访问令牌出错", 5 | "Error during OAuth exchanges" : "OAuth 交换期间出错", 6 | "Bad HTTP method" : "错误的HTTP方法", 7 | "OAuth access token refused" : "OAuth 访问令牌被拒绝", 8 | "Cancel" : "取消", 9 | "Authentication method" : "认证方法", 10 | "Save" : "保存", 11 | "Incorrect access token" : "访问令牌不正确", 12 | "Invalid token" : "令牌无效", 13 | "Connected as {user}" : "以 {user} 身份连接", 14 | "Enable navigation link" : "启用应用图标链接至实例", 15 | "Enable unified search for tickets" : "启用统一的票证搜索", 16 | "URL is invalid" : "URL无效", 17 | "Documentation" : "文档", 18 | "Copied!" : "已复制!", 19 | "Copied to the clipboard" : "已复制到剪切板", 20 | "Details" : "详情", 21 | "Start typing to search" : "开始输入以搜索", 22 | "Select a user or group" : "选择用户或分组", 23 | "Description" : "描述", 24 | "Create" : "创建", 25 | "Mark as read" : "标为已读", 26 | "Unlink" : "取消关联", 27 | "OAuth access token could not be obtained:" : "无法获取 OAuth 访问令牌:", 28 | "Connected accounts" : "关联账号", 29 | "Reset" : "重置" 30 | }, 31 | "nplurals=1; plural=0;"); 32 | -------------------------------------------------------------------------------- /l10n/zh_CN.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Error getting OAuth access token" : "获取 OAUth 访问令牌出错", 3 | "Error during OAuth exchanges" : "OAuth 交换期间出错", 4 | "Bad HTTP method" : "错误的HTTP方法", 5 | "OAuth access token refused" : "OAuth 访问令牌被拒绝", 6 | "Cancel" : "取消", 7 | "Authentication method" : "认证方法", 8 | "Save" : "保存", 9 | "Incorrect access token" : "访问令牌不正确", 10 | "Invalid token" : "令牌无效", 11 | "Connected as {user}" : "以 {user} 身份连接", 12 | "Enable navigation link" : "启用应用图标链接至实例", 13 | "Enable unified search for tickets" : "启用统一的票证搜索", 14 | "URL is invalid" : "URL无效", 15 | "Documentation" : "文档", 16 | "Copied!" : "已复制!", 17 | "Copied to the clipboard" : "已复制到剪切板", 18 | "Details" : "详情", 19 | "Start typing to search" : "开始输入以搜索", 20 | "Select a user or group" : "选择用户或分组", 21 | "Description" : "描述", 22 | "Create" : "创建", 23 | "Mark as read" : "标为已读", 24 | "Unlink" : "取消关联", 25 | "OAuth access token could not be obtained:" : "无法获取 OAuth 访问令牌:", 26 | "Connected accounts" : "关联账号", 27 | "Reset" : "重置" 28 | },"pluralForm" :"nplurals=1; plural=0;" 29 | } -------------------------------------------------------------------------------- /l10n/zh_TW.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "integration_openproject", 3 | { 4 | "OpenProject" : "OpenProject", 5 | "Error getting OAuth access token" : "取得 OAuth 存取權杖時發生錯誤", 6 | "Error during OAuth exchanges" : "OAuth 交換時發生錯誤", 7 | "Bad HTTP method" : "錯誤的 HTTP 方法", 8 | "OAuth access token refused" : "OAuth 存取權杖被拒絕", 9 | "Cancel" : "取消", 10 | "Authentication method" : "認證方式", 11 | "Save" : "儲存", 12 | "Incorrect access token" : "不正確的存取權杖", 13 | "Invalid token" : "無效的權杖", 14 | "Connected as {user}" : "以 {user} 身份連線", 15 | "Enable navigation link" : "啟用導覽連結", 16 | "Documentation" : "文件", 17 | "Copied!" : "已複製!", 18 | "Copied to the clipboard" : "已複製到剪貼簿", 19 | "Details" : "詳細資訊", 20 | "Start typing to search" : "開始輸入以搜尋", 21 | "Select a user or group" : "選取使用者或群組", 22 | "Description" : "描述", 23 | "Create" : "建立", 24 | "Mark as read" : "標為已讀", 25 | "Unlink" : "取消連結", 26 | "OAuth access token could not be obtained:" : "無法取得 OAuth 存取權杖:", 27 | "Connected accounts" : "已連線的帳號", 28 | "Reset" : "重設" 29 | }, 30 | "nplurals=1; plural=0;"); 31 | -------------------------------------------------------------------------------- /l10n/zh_TW.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "OpenProject" : "OpenProject", 3 | "Error getting OAuth access token" : "取得 OAuth 存取權杖時發生錯誤", 4 | "Error during OAuth exchanges" : "OAuth 交換時發生錯誤", 5 | "Bad HTTP method" : "錯誤的 HTTP 方法", 6 | "OAuth access token refused" : "OAuth 存取權杖被拒絕", 7 | "Cancel" : "取消", 8 | "Authentication method" : "認證方式", 9 | "Save" : "儲存", 10 | "Incorrect access token" : "不正確的存取權杖", 11 | "Invalid token" : "無效的權杖", 12 | "Connected as {user}" : "以 {user} 身份連線", 13 | "Enable navigation link" : "啟用導覽連結", 14 | "Documentation" : "文件", 15 | "Copied!" : "已複製!", 16 | "Copied to the clipboard" : "已複製到剪貼簿", 17 | "Details" : "詳細資訊", 18 | "Start typing to search" : "開始輸入以搜尋", 19 | "Select a user or group" : "選取使用者或群組", 20 | "Description" : "描述", 21 | "Create" : "建立", 22 | "Mark as read" : "標為已讀", 23 | "Unlink" : "取消連結", 24 | "OAuth access token could not be obtained:" : "無法取得 OAuth 存取權杖:", 25 | "Connected accounts" : "已連線的帳號", 26 | "Reset" : "重設" 27 | },"pluralForm" :"nplurals=1; plural=0;" 28 | } -------------------------------------------------------------------------------- /lib/BackgroundJob/RemoveExpiredDirectUploadTokens.php: -------------------------------------------------------------------------------- 1 | setInterval(24 * 3600); 31 | $this->databaseService = $databaseService; 32 | $this->logger = $logger; 33 | } 34 | 35 | /** 36 | * @param mixed $argument 37 | * @return void 38 | * @throws Exception 39 | */ 40 | public function run($argument): void { 41 | $this->databaseService->deleteExpiredTokens(); 42 | $this->logger->info('Deleted all the expired tokens from Database'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/Capabilities.php: -------------------------------------------------------------------------------- 1 | appManager = $appManager; 25 | } 26 | 27 | /** 28 | * @return array> 29 | */ 30 | public function getCapabilities(): array { 31 | $appVersion = $this->appManager->getAppVersion(Application::APP_ID); 32 | $groupfoldersVersion = $this->appManager->getAppVersion('groupfolders'); 33 | $groupfoldersEnabled = $this->appManager->isEnabledForUser('groupfolders'); 34 | return [ 35 | Application::APP_ID => [ 36 | 'app_version' => $appVersion, 37 | 'groupfolder_version' => $groupfoldersVersion, 38 | 'groupfolders_enabled' => $groupfoldersEnabled, 39 | ], 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/Controller/DirectDownloadController.php: -------------------------------------------------------------------------------- 1 | l = $l; 35 | $this->directDownloadService = $directDownloadService; 36 | } 37 | 38 | /** 39 | * Direct download 40 | * @NoCSRFRequired 41 | * @PublicPage 42 | * 43 | * @param string $token 44 | * @param string $fileName 45 | * @return DataDownloadResponse|TemplateResponse 46 | * @throws \OCP\Files\NotPermittedException 47 | * @throws \OCP\Lock\LockedException 48 | */ 49 | public function directDownload(string $token, string $fileName) { 50 | $file = $this->directDownloadService->getDirectDownloadFile($token); 51 | if ($file !== null) { 52 | return new DataDownloadResponse($file->getContent(), $fileName, $file->getMimeType()); 53 | } 54 | return new TemplateResponse('core', 'error', [ 55 | 'errors' => [ 56 | [ 57 | 'error' => $this->l->t('Direct download error'), 58 | 'hint' => $this->l->t('This direct download link is invalid or has expired'), 59 | ], 60 | ], 61 | ], TemplateResponse::RENDER_AS_GUEST); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/Exception/OpenprojectAvatarErrorException.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class BeforeGroupDeletedListener implements IEventListener { 23 | 24 | /** 25 | * @var LoggerInterface 26 | */ 27 | private $logger; 28 | 29 | public function __construct(LoggerInterface $logger) { 30 | $this->logger = $logger; 31 | } 32 | 33 | 34 | /** 35 | * @throws \Exception 36 | */ 37 | public function handle(Event $event): void { 38 | if (!($event instanceof BeforeGroupDeletedEvent)) { 39 | return; 40 | } 41 | 42 | $group = $event->getGroup(); 43 | if ($group->getGID() === Application::OPEN_PROJECT_ENTITIES_NAME) { 44 | $this->logger->error('Group "OpenProject" is needed to be protected by the app "OpenProject Integration", thus cannot be deleted. Please check the documentation "https://www.openproject.org/docs/system-admin-guide/integrations/nextcloud/#troubleshooting" for further information.'); 45 | throw new OCSBadRequestException('

 Group "OpenProject" is needed to be protected by the app "OpenProject Integration", thus cannot be deleted. 46 | Please check the troubleshooting guide for further information.

'); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/Listener/BeforeNodeInsideOpenProjectGroupfilderChangedListener.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class BeforeNodeInsideOpenProjectGroupfilderChangedListener implements IEventListener { 26 | /** 27 | * @var OpenProjectAPIService 28 | */ 29 | private $openprojectAPIService; 30 | 31 | /** 32 | * @var IGroupManager 33 | */ 34 | private $groupManager; 35 | 36 | /** 37 | * @var IUserSession 38 | */ 39 | private $userSession; 40 | 41 | public function __construct( 42 | OpenProjectAPIService $openprojectAPIService, 43 | IUserSession $userSession, 44 | IGroupManager $groupManager 45 | ) { 46 | $this->openprojectAPIService = $openprojectAPIService; 47 | $this->userSession = $userSession; 48 | $this->groupManager = $groupManager; 49 | } 50 | 51 | public function handle(Event $event): void { 52 | if (($event instanceof BeforeNodeDeletedEvent)) { 53 | $parentNode = $event->getNode()->getParent(); 54 | } elseif (($event instanceof BeforeNodeRenamedEvent)) { 55 | $parentNode = $event->getSource()->getParent(); 56 | } else { 57 | return; 58 | } 59 | $currentUser = $this->userSession->getUser(); 60 | // we do not listen event where user is not logged or there is no user session (e.g. public link ) 61 | // or if it's some background job in which case user will be null 62 | if (OC_User::isIncognitoMode() || $currentUser === null) { 63 | return; 64 | } 65 | $currentUserId = $currentUser->getUID(); 66 | if ( 67 | $this->openprojectAPIService->isProjectFoldersSetupComplete() && 68 | preg_match('/.*\/files\/' . Application::OPEN_PROJECT_ENTITIES_NAME . '$/', $parentNode->getPath()) === 1 && 69 | $currentUserId !== Application::OPEN_PROJECT_ENTITIES_NAME && 70 | $this->groupManager->isInGroup($currentUserId, Application::OPEN_PROJECT_ENTITIES_NAME) 71 | ) { 72 | if (!class_exists("\OCP\HintException")) { 73 | throw new \OCP\HintException( 74 | 'project folders cannot be deleted or renamed' 75 | ); 76 | } 77 | throw new \OC\HintException( 78 | 'project folders cannot be deleted or renamed' 79 | ); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/Listener/BeforeUserDeletedListener.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class BeforeUserDeletedListener implements IEventListener { 23 | 24 | /** 25 | * @var LoggerInterface 26 | */ 27 | private $logger; 28 | 29 | public function __construct(LoggerInterface $logger) { 30 | $this->logger = $logger; 31 | } 32 | 33 | /** 34 | * @throws \Exception 35 | */ 36 | public function handle(Event $event): void { 37 | if (!($event instanceof BeforeUserDeletedEvent)) { 38 | return; 39 | } 40 | $user = $event->getUser(); 41 | if ($user->getUID() === Application::OPEN_PROJECT_ENTITIES_NAME) { 42 | $this->logger->error('User "OpenProject" is needed to be protected by the app "OpenProject Integration", thus cannot be deleted. Please check the documentation "https://www.openproject.org/docs/system-admin-guide/integrations/nextcloud/#troubleshooting" for further information.'); 43 | throw new OCSBadRequestException('

 User "OpenProject" is needed to be protected by the app "OpenProject Integration", thus cannot be deleted. 44 | Please check the troubleshooting guide for further information.

'); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/Listener/LoadAdditionalScriptsListener.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class LoadAdditionalScriptsListener implements IEventListener { 23 | 24 | /** 25 | * @var OpenProjectAPIService 26 | */ 27 | private $openProjectAPIService; 28 | /** 29 | * @var IConfig 30 | */ 31 | private $config; 32 | 33 | public function __construct( 34 | IConfig $config, 35 | OpenProjectAPIService $openProjectAPIService, 36 | ) { 37 | $this->config = $config; 38 | $this->openProjectAPIService = $openProjectAPIService; 39 | } 40 | 41 | public function handle(Event $event): void { 42 | // When user is non oidc based or there is some error when getting token for the targeted client 43 | // then we need to hide the oidc based connection for the user 44 | // so this check is required 45 | if ( 46 | $this->config->getAppValue(Application::APP_ID, 'authorization_method', '') === OpenProjectAPIService::AUTH_METHOD_OIDC && 47 | !$this->openProjectAPIService->getOIDCToken() 48 | ) { 49 | return; 50 | } 51 | if (!$event instanceof LoadAdditionalScriptsEvent) { 52 | return; 53 | } 54 | if (version_compare(ServerVersionHelper::getNextcloudVersion(), '28') < 0) { 55 | Util::addScript(Application::APP_ID, Application::APP_ID . '-fileActions'); 56 | Util::addScript(Application::APP_ID, Application::APP_ID . '-filesPluginLessThan28', 'files'); 57 | } else { 58 | Util::addScript(Application::APP_ID, Application::APP_ID . '-filesPlugin'); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/Listener/OpenProjectReferenceListener.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class OpenProjectReferenceListener implements IEventListener { 24 | 25 | /** 26 | * @var IInitialState 27 | */ 28 | private $initialStateService; 29 | 30 | /** 31 | * @var IConfig 32 | */ 33 | private $config; 34 | /** 35 | * @var OpenProjectAPIService 36 | */ 37 | private $openProjectAPIService; 38 | 39 | public function __construct( 40 | IInitialState $initialStateService, 41 | IConfig $config, 42 | OpenProjectAPIService $openProjectAPIService, 43 | ) { 44 | $this->initialStateService = $initialStateService; 45 | $this->config = $config; 46 | $this->openProjectAPIService = $openProjectAPIService; 47 | } 48 | public function handle(Event $event): void { 49 | // When user is non oidc based or there is some error when getting token for the targeted client 50 | // then we need to hide the oidc based connection for the user 51 | // so this check is required 52 | if ( 53 | $this->config->getAppValue(Application::APP_ID, 'authorization_method', '') === OpenProjectAPIService::AUTH_METHOD_OIDC && 54 | !$this->openProjectAPIService->getOIDCToken() 55 | ) { 56 | return; 57 | } 58 | if (!$event instanceof RenderReferenceEvent) { 59 | return; 60 | } 61 | Util::addScript(Application::APP_ID, Application::APP_ID . '-reference'); 62 | $adminConfig = [ 63 | 'isAdminConfigOk' => OpenProjectAPIService::isAdminConfigOk($this->config), 64 | 'authMethod' => $this->config->getAppValue(Application::APP_ID, 'authorization_method', '') 65 | ]; 66 | $this->initialStateService->provideInitialState( 67 | 'admin-config', 68 | $adminConfig 69 | ); 70 | $this->initialStateService->provideInitialState( 71 | 'openproject-url', 72 | $this->config->getAppValue(Application::APP_ID, 'openproject_instance_url') 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/Listener/TermsOfServiceEventListener.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class TermsOfServiceEventListener implements IEventListener { 26 | 27 | /** 28 | * @var OpenProjectAPIService 29 | */ 30 | private OpenProjectAPIService $openprojectAPIService; 31 | 32 | /** 33 | * @var LoggerInterface 34 | */ 35 | private $logger; 36 | 37 | public function __construct( 38 | OpenProjectAPIService $openprojectAPIService, 39 | LoggerInterface $logger 40 | ) { 41 | $this->openprojectAPIService = $openprojectAPIService; 42 | $this->logger = $logger; 43 | } 44 | 45 | public function handle(Event $event): void { 46 | try { 47 | if ($event instanceof TermsCreatedEvent) { 48 | $this->openprojectAPIService->signTermsOfServiceForUserOpenProject(); 49 | } 50 | if ($event instanceof SignaturesResetEvent) { 51 | $this->openprojectAPIService->signTermsOfServiceForUserOpenProject(); 52 | } 53 | if ($event instanceof AppEnableEvent) { 54 | if ($event->getAppId() === 'terms_of_service') { 55 | $this->openprojectAPIService->signTermsOfServiceForUserOpenProject(); 56 | } 57 | } 58 | } catch (DBException $e) { 59 | $this->logger->error( 60 | 'Error: ' . $e->getMessage(), 61 | ['app' => Application::APP_ID] 62 | ); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/Listener/UserChangedListener.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class UserChangedListener implements IEventListener { 23 | 24 | /** 25 | * @var LoggerInterface 26 | */ 27 | private $logger; 28 | 29 | 30 | /** 31 | * @param LoggerInterface $logger 32 | */ 33 | public function __construct(LoggerInterface $logger) { 34 | $this->logger = $logger; 35 | } 36 | 37 | /** 38 | * @throws \Exception 39 | */ 40 | public function handle(Event $event): void { 41 | if (!($event instanceof UserChangedEvent)) { 42 | return; 43 | } 44 | 45 | if ($event->getUser()->getUID() === Application::OPEN_PROJECT_ENTITIES_NAME) { 46 | $feature = $event->getFeature(); 47 | if ($feature === 'enabled' && !$event->getValue()) { 48 | $this->logger->error('User "OpenProject" is needed to be protected by the app "OpenProject Integration", thus cannot be disabled. Please check the documentation "https://www.openproject.org/docs/system-admin-guide/integrations/nextcloud/#troubleshooting" for further information.'); 49 | $event->getUser()->setEnabled(); 50 | throw new OCSBadRequestException('

 User "OpenProject" is needed to be protected by the 51 | app "OpenProject Integration", thus cannot be disabled. 52 | Please check the troubleshooting guide for further information.

'); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/Migration/Version2001Date20221213083550.php: -------------------------------------------------------------------------------- 1 | config = $config; 31 | } 32 | 33 | /** 34 | * @param IOutput $output 35 | * @param Closure(): ISchemaWrapper $schemaClosure 36 | * @param array $options 37 | * @return null|ISchemaWrapper 38 | */ 39 | public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { 40 | $oldOpenProjectOauthUrl = $this->config->getAppValue( 41 | Application::APP_ID, 'oauth_instance_url', '' 42 | ); 43 | $oldClientId = $this->config->getAppValue( 44 | Application::APP_ID, 'client_id', '' 45 | ); 46 | $oldClientSecret = $this->config->getAppValue( 47 | Application::APP_ID, 'client_secret', '' 48 | ); 49 | 50 | $this->config->setAppValue( 51 | Application::APP_ID, 'openproject_instance_url', $oldOpenProjectOauthUrl 52 | ); 53 | $this->config->setAppValue( 54 | Application::APP_ID, 'openproject_client_id', $oldClientId 55 | ); 56 | $this->config->setAppValue( 57 | Application::APP_ID, 'openproject_client_secret', $oldClientSecret 58 | ); 59 | 60 | $this->config->deleteAppValue(Application::APP_ID, 'oauth_instance_url'); 61 | $this->config->deleteAppValue(Application::APP_ID, 'client_id'); 62 | $this->config->deleteAppValue(Application::APP_ID, 'client_secret'); 63 | 64 | return null; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/Migration/Version2310Date20230116153411.php: -------------------------------------------------------------------------------- 1 | hasTable('directUpload')) { 33 | $schema->dropTable('directUpload'); 34 | } elseif ($schema->hasTable('directupload')) { 35 | $schema->dropTable('directupload'); 36 | } 37 | if (!$schema->hasTable('direct_upload')) { 38 | $table = $schema->createTable('direct_upload'); 39 | $table->addColumn('id', 'integer', [ 40 | 'autoincrement' => true, 41 | 'notnull' => true, 42 | ]); 43 | $table->addColumn('token', 'string', [ 44 | 'notnull' => true, 45 | 'length' => 64 46 | ]); 47 | $table->addColumn('created_at', 'bigint', [ 48 | 'notnull' => true, 49 | 'unsigned' => true 50 | ]); 51 | $table->addColumn('expires_on', 'bigint', [ 52 | 'notnull' => true, 53 | 'unsigned' => true 54 | ]); 55 | $table->addColumn('folder_id', 'bigint', [ 56 | 'notnull' => true, 57 | 'length' => 20, 58 | ]); 59 | $table->addColumn('user_id', 'string', [ 60 | 'notnull' => true, 61 | 'length' => 64, 62 | ]); 63 | 64 | $table->setPrimaryKey(['id']); 65 | $table->addIndex(['token'], 'direct_upload_token_index'); 66 | } 67 | return $schema; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/Migration/Version2400Date20230504144300.php: -------------------------------------------------------------------------------- 1 | config = $config; 31 | } 32 | 33 | /** 34 | * @param IOutput $output 35 | * @param Closure(): ISchemaWrapper $schemaClosure 36 | * @param array $options 37 | * @return null|ISchemaWrapper 38 | */ 39 | public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { 40 | $this->config->setAppValue( 41 | Application::APP_ID, 'fresh_project_folder_setup', "1" 42 | ); 43 | return null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/Migration/Version2640Date20240628114301.php: -------------------------------------------------------------------------------- 1 | mapper = $mapper; 36 | } 37 | 38 | /** 39 | * @param IOutput $output 40 | * @param Closure(): ISchemaWrapper $schemaClosure 41 | * @param array $options 42 | * @return null|ISchemaWrapper 43 | * @throws DoesNotExistException 44 | * @throws Exception 45 | */ 46 | public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { 47 | $tokens = $this->mapper->getTokenByUser(Application::OPEN_PROJECT_ENTITIES_NAME); 48 | foreach ($tokens as $token) { 49 | if ($token->getName() === Application::OPEN_PROJECT_ENTITIES_NAME) { 50 | // We convert current "OpenProject" user with temporary app password token types to permanent one. 51 | // type 0 => Temporary app password token where as type 1 => Permanent app password token 52 | $token->setType(1); 53 | $this->mapper->update($token); 54 | } 55 | } 56 | return null; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/OIDCClientMapper.php: -------------------------------------------------------------------------------- 1 | clientMapper)) { 40 | $this->clientMapper = new ClientMapper( 41 | $this->db, 42 | $this->timeFactory, 43 | $this->appConfig, 44 | new RedirectUriMapper($this->db, $this->timeFactory, $this->appConfig), 45 | $this->random, 46 | $this->logger, 47 | ); 48 | } 49 | 50 | return $this->clientMapper->getByIdentifier($clientIdentifier); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/Search/OpenProjectSearchResultEntry.php: -------------------------------------------------------------------------------- 1 | getVersion(); 25 | } else { 26 | /** @psalm-suppress UndefinedMethod getVersion() method is not in stable31 so making psalm not complain */ 27 | $versionArray = OC_Util::getVersion(); 28 | } 29 | 30 | return implode('.', $versionArray); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/Service/DirectDownloadService.php: -------------------------------------------------------------------------------- 1 | db = $db; 33 | $this->root = $root; 34 | } 35 | 36 | public function getDirectDownloadFile(string $token): ?File { 37 | $qb = $this->db->getQueryBuilder(); 38 | 39 | $qb->select('id', 'user_id', 'file_id', 'token', 'expiration') 40 | ->from('directlink') 41 | ->where( 42 | $qb->expr()->eq('token', $qb->createNamedParameter($token, IQueryBuilder::PARAM_STR)) 43 | ); 44 | $req = $qb->executeQuery(); 45 | 46 | while ($row = $req->fetch()) { 47 | $dbFileId = (int) $row['file_id']; 48 | $dbExpiration = (int) $row['expiration']; 49 | $dbUserId = $row['user_id']; 50 | 51 | $nowTs = (new DateTime())->getTimestamp(); 52 | if ($nowTs <= $dbExpiration) { 53 | $userFolder = $this->root->getUserFolder($dbUserId); 54 | $files = $userFolder->getById($dbFileId); 55 | if (count($files) > 0 && $files[0] instanceof File) { 56 | $req->closeCursor(); 57 | $qb->resetQueryParts(); 58 | return $files[0]; 59 | } 60 | } 61 | } 62 | $req->closeCursor(); 63 | $qb->resetQueryParts(); 64 | 65 | return null; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/Settings/AdminSection.php: -------------------------------------------------------------------------------- 1 | l = $l; 26 | $this->urlGenerator = $urlGenerator; 27 | } 28 | 29 | /** 30 | * returns the ID of the section. It is supposed to be a lower case string 31 | * 32 | * @returns string 33 | */ 34 | public function getID(): string { 35 | return 'openproject'; 36 | } 37 | 38 | /** 39 | * returns the translated name as it should be displayed, e.g. 'LDAP / AD 40 | * integration'. Use the L10N service to translate it. 41 | * 42 | * @return string 43 | */ 44 | public function getName(): string { 45 | return $this->l->t('OpenProject'); 46 | } 47 | 48 | /** 49 | * @return int whether the form should be rather on the top or bottom of 50 | * the settings navigation. The sections are arranged in ascending order of 51 | * the priority values. It is required to return a value between 0 and 99. 52 | */ 53 | public function getPriority(): int { 54 | return 80; 55 | } 56 | 57 | /** 58 | * @return string The relative path to a an icon describing the section 59 | */ 60 | public function getIcon(): string { 61 | return $this->urlGenerator->imagePath('integration_openproject', 'app-dark.svg'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/Settings/PersonalSection.php: -------------------------------------------------------------------------------- 1 | l = $l; 26 | $this->urlGenerator = $urlGenerator; 27 | } 28 | 29 | /** 30 | * returns the ID of the section. It is supposed to be a lower case string 31 | * 32 | * @returns string 33 | */ 34 | public function getID(): string { 35 | return 'openproject'; //or a generic id if feasible 36 | } 37 | 38 | /** 39 | * returns the translated name as it should be displayed, e.g. 'LDAP / AD 40 | * integration'. Use the L10N service to translate it. 41 | * 42 | * @return string 43 | */ 44 | public function getName(): string { 45 | return $this->l->t('OpenProject'); 46 | } 47 | 48 | /** 49 | * @return int whether the form should be rather on the top or bottom of 50 | * the settings navigation. The sections are arranged in ascending order of 51 | * the priority values. It is required to return a value between 0 and 99. 52 | */ 53 | public function getPriority(): int { 54 | return 80; 55 | } 56 | 57 | /** 58 | * @return string The relative path to a an icon describing the section 59 | */ 60 | public function getIcon(): string { 61 | return $this->urlGenerator->imagePath('integration_openproject', 'app-dark.svg'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/TokenEventFactory.php: -------------------------------------------------------------------------------- 1 | config = $config; 24 | } 25 | 26 | /** 27 | * @return InternalTokenEvent|ExternalTokenEvent|ExchangedTokenEvent 28 | */ 29 | public function getEvent(): InternalTokenEvent|ExternalTokenEvent|ExchangedTokenEvent { 30 | $SSOProviderType = $this->config->getAppValue(Application::APP_ID, 'sso_provider_type', ''); 31 | $tokenExchange = $this->config->getAppValue(Application::APP_ID, 'token_exchange', ''); 32 | $targetAudience = $this->config->getAppValue(Application::APP_ID, 'targeted_audience_client_id', ''); 33 | 34 | // If the SSO provider is Nextcloud Hub, 35 | // get token from internal IDP (oidc) 36 | if ($SSOProviderType === OpenProjectAPIService::NEXTCLOUD_HUB_PROVIDER) { 37 | return new InternalTokenEvent($targetAudience, Application::OPENPROJECT_API_SCOPES, $targetAudience); 38 | } 39 | 40 | // If the SSO provider is external and token exchange is disabled, 41 | // get the login token 42 | if (!$tokenExchange) { 43 | // NOTE: cannot request new scopes with ExternalTokenEvent 44 | return new ExternalTokenEvent(); 45 | } 46 | 47 | // If the SSO provider is external and token exchange is enabled, 48 | // exchange the token for targeted audience client 49 | return new ExchangedTokenEvent($targetAudience, Application::OPENPROJECT_API_SCOPES); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration_openproject", 3 | "version": "2.10.0-alpha.1", 4 | "description": "OpenProject Integration", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "build": "NODE_ENV=production webpack --progress --config webpack.js", 11 | "dev": "NODE_ENV=development webpack --progress --config webpack.js", 12 | "watch": "NODE_ENV=development webpack --progress --watch --config webpack.js", 13 | "lint": "eslint --ext .js,.vue src tests", 14 | "lint:fix": "eslint --ext .js,.vue src tests --fix", 15 | "stylelint": "stylelint css src", 16 | "stylelint:fix": "stylelint css src --fix", 17 | "test:unit": "jest --silent", 18 | "test:unit:watch": "jest --watch --no-coverage" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/nextcloud/integration_openproject" 23 | }, 24 | "keywords": [ 25 | "openproject" 26 | ], 27 | "author": "Nextcloud GmbH and Nextcloud contributors", 28 | "license": "AGPL-3.0-or-later", 29 | "bugs": { 30 | "url": "https://github.com/nextcloud/integration_openproject/issues" 31 | }, 32 | "homepage": "https://github.com/nextcloud/integration_openproject", 33 | "browserslist": [ 34 | "extends @nextcloud/browserslist-config" 35 | ], 36 | "engines": { 37 | "node": ">= 16.0.0", 38 | "npm": "^8.0.0" 39 | }, 40 | "dependencies": { 41 | "@mdi/svg": "^7.3.67", 42 | "@nextcloud/auth": "^2.3.0", 43 | "@nextcloud/axios": "^2.5.0", 44 | "@nextcloud/dialogs": "^5.3.4", 45 | "@nextcloud/files": "^3.5.1", 46 | "@nextcloud/initial-state": "^2.2.0", 47 | "@nextcloud/l10n": "^2.2.0", 48 | "@nextcloud/moment": "^1.3.1", 49 | "@nextcloud/router": "^3.0.1", 50 | "@nextcloud/vue": "^8.12.0", 51 | "dompurify": "^3.1.5", 52 | "lodash": "^4.17.21", 53 | "vue": "^2.7.16", 54 | "vue-material-design-icons": "^5.3.0" 55 | }, 56 | "devDependencies": { 57 | "@babel/core": "^7.24.3", 58 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 59 | "@babel/preset-env": "^7.24.3", 60 | "@nextcloud/browserslist-config": "^3.0.0", 61 | "@nextcloud/eslint-config": "^8.4.1", 62 | "@nextcloud/stylelint-config": "^2.3.0", 63 | "@nextcloud/webpack-vue-config": "^6.0.1", 64 | "@vue/test-utils": "^1.3.6", 65 | "@vue/vue2-jest": "^29.2.6", 66 | "babel-jest": "^29.0.0", 67 | "eslint-webpack-plugin": "^4.2.0", 68 | "flush-promises": "^1.0.2", 69 | "jest-environment-jsdom": "^29.7.0", 70 | "jest-transform-stub": "^2.0.0", 71 | "stylelint-webpack-plugin": "^5.0.1", 72 | "vue-loader": "^15.11.1", 73 | "vue-template-compiler": "^2.7.16" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | lib 19 | 20 | 21 | 22 | 23 | 24 | tests/lib/ 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/adminSettings.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion: 6 */ 2 | 3 | /** 4 | * SPDX-FileCopyrightText: 2021-2023 Nextcloud GmbH and Nextcloud contributors 5 | * SPDX-License-Identifier: AGPL-3.0-or-later 6 | */ 7 | 8 | import Vue from 'vue' 9 | import './bootstrap.js' 10 | import AdminSettings from './components/AdminSettings.vue' 11 | 12 | // eslint-disable-next-line 13 | 'use strict' 14 | 15 | // eslint-disable-next-line 16 | new Vue({ 17 | el: '#openproject_prefs', 18 | render: h => h(AdminSettings), 19 | }) 20 | -------------------------------------------------------------------------------- /src/api/endpoints.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2025 Jankari Tech Pvt. Ltd. 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { generateUrl } from '@nextcloud/router' 7 | 8 | export default { 9 | validateOPInstance: generateUrl('/apps/integration_openproject/is-valid-op-instance'), 10 | adminConfig: generateUrl('/apps/integration_openproject/admin-config'), 11 | } 12 | -------------------------------------------------------------------------------- /src/api/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2025 Jankari Tech Pvt. Ltd. 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import axios from '@nextcloud/axios' 7 | import endpoints from './endpoints.js' 8 | 9 | export function validateOPInstance(url) { 10 | return axios.post(endpoints.validateOPInstance, { url }) 11 | } 12 | 13 | export function saveAdminConfig(configs) { 14 | return axios.put(endpoints.adminConfig, { values: configs }) 15 | } 16 | -------------------------------------------------------------------------------- /src/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import Vue from 'vue' 7 | import { translate, translatePlural } from '@nextcloud/l10n' 8 | 9 | Vue.prototype.t = translate 10 | Vue.prototype.n = translatePlural 11 | Vue.prototype.OC = window.OC 12 | Vue.prototype.OCA = window.OCA 13 | -------------------------------------------------------------------------------- /src/components/ErrorLabel.vue: -------------------------------------------------------------------------------- 1 | 5 | 10 | 11 | 26 | 27 | 36 | -------------------------------------------------------------------------------- /src/components/admin/FieldValue.vue: -------------------------------------------------------------------------------- 1 | 6 | 26 | 86 | 96 | -------------------------------------------------------------------------------- /src/components/icons/ClippyIcon.vue: -------------------------------------------------------------------------------- 1 | 5 | 25 | 26 | 45 | -------------------------------------------------------------------------------- /src/components/icons/OpenProjectIcon.vue: -------------------------------------------------------------------------------- 1 | 5 | 24 | 25 | 44 | -------------------------------------------------------------------------------- /src/components/settings/CheckBox.vue: -------------------------------------------------------------------------------- 1 | 5 | 23 | 42 | 47 | -------------------------------------------------------------------------------- /src/components/settings/ErrorNote.vue: -------------------------------------------------------------------------------- 1 | 5 | 19 | 20 | 48 | 49 | 59 | -------------------------------------------------------------------------------- /src/constants/appID.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2025 Jankari Tech Pvt. Ltd. 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | const APP_ID = 'integration_openproject' 7 | 8 | export default APP_ID 9 | -------------------------------------------------------------------------------- /src/constants/links.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2025 Jankari Tech Pvt. Ltd. 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { generateUrl } from '@nextcloud/router' 7 | 8 | export const appLinks = { 9 | user_oidc: { installLink: generateUrl('/settings/apps/files/user_oidc'), settingsLink: generateUrl('settings/admin/user_oidc') }, 10 | } 11 | -------------------------------------------------------------------------------- /src/constants/messages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2025 Jankari Tech Pvt. Ltd. 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { translate as t } from '@nextcloud/l10n' 7 | import APP_ID from './appID.js' 8 | 9 | export const messages = { 10 | appRequiredForOIDCMethod: t(APP_ID, 'This app is required to use the OIDC authentication method'), 11 | downloadAndEnableApp: t(APP_ID, 'Download and enable it'), 12 | featureNotAvailable: t(APP_ID, 'This feature is not available for this user account'), 13 | opConnectionUnauthorized: t(APP_ID, 'Unauthorized to connect to OpenProject'), 14 | opClientId: t(APP_ID, 'OpenProject client ID'), 15 | tokenExchangeFormLabel: t(APP_ID, 'Token Exchange'), 16 | enableTokenExchange: t(APP_ID, 'Enable token exchange'), 17 | opClientIdHintText: t(APP_ID, 'You can get this value from your identity provider when you configure the client'), 18 | nextcloudHubProvider: t(APP_ID, 'Nextcloud Hub'), 19 | externalOIDCProvider: t(APP_ID, 'External Provider'), 20 | tokenExchangeHintText: t(APP_ID, 'When enabled, the app will try to obtain a token for the given audience from the identity provider. If disabled, it will use the access token obtained during the login process.'), 21 | opRequiredVersionAndPlanHint: t(APP_ID, 'Requires OpenProject version 16.0 (or higher) and an active Corporate plan.'), 22 | } 23 | 24 | export const messagesFmt = { 25 | appNotInstalled: (app) => t(APP_ID, 'The "{app}" app is not installed', { app }), 26 | appNotSupported: (app) => t(APP_ID, 'The "{app}" app is not supported', { app }), 27 | appNotEnabledOrSupported: (app) => t(APP_ID, 'The "{app}" app is not enabled or supported', { app }), 28 | minimumVersionRequired: (minimumAppVersion) => t(APP_ID, 'Requires app version "{minimumAppVersion}" or later', { minimumAppVersion }), 29 | configureOIDCProviders: (settingsLink) => t(APP_ID, 'You can configure OIDC providers in the {settingsLink}', { settingsLink }, null, { escape: false, sanitize: false }), 30 | } 31 | -------------------------------------------------------------------------------- /src/dashboard.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2021-2023 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import Vue from 'vue' 7 | import './bootstrap.js' 8 | import Dashboard from './views/Dashboard.vue' 9 | 10 | document.addEventListener('DOMContentLoaded', function() { 11 | 12 | OCA.Dashboard.register('openproject_notifications', (el, { widget }) => { 13 | const View = Vue.extend(Dashboard) 14 | new View({ 15 | propsData: { title: widget.title }, 16 | }).$mount(el) 17 | }) 18 | 19 | }) 20 | -------------------------------------------------------------------------------- /src/fileActions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-FileCopyrightText: 2022 Jankari Tech Pvt. Ltd. 4 | * SPDX-License-Identifier: AGPL-3.0-or-later 5 | */ 6 | 7 | import './bootstrap.js' 8 | 9 | OCA.Files.fileActions.registerAction({ 10 | name: 'open-project', 11 | displayName: t('integration_openproject', 'OpenProject'), 12 | mime: 'all', 13 | permissions: OC.PERMISSION_READ, 14 | iconClass: 'icon-openproject', 15 | actionHandler: (filename, context) => { 16 | const fileList = context.fileList 17 | if (!fileList._detailsView) { 18 | return 19 | } 20 | // use the sidebar-tab id for the navigation 21 | fileList.showDetailsView(filename, 'open-project') 22 | }, 23 | }) 24 | -------------------------------------------------------------------------------- /src/filesPlugin/filesPluginLessThan28.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Jankari Tech Pvt. Ltd. 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import '../bootstrap.js' 7 | import Vue from 'vue' 8 | import LinkMultipleFilesModal from '../views/LinkMultipleFilesModal.vue' 9 | 10 | (function() { 11 | if (!OCA.OpenProject) { 12 | /** 13 | * @namespace 14 | */ 15 | OCA.OpenProject = { 16 | requestOnFileChange: false, 17 | } 18 | } 19 | 20 | /** 21 | * @namespace 22 | */ 23 | OCA.OpenProject.FilesPlugin = { 24 | ignoreLists: [ 25 | 'trashbin', 26 | 'files.public', 27 | ], 28 | 29 | attach(fileList) { 30 | if (this.ignoreLists.indexOf(fileList.id) >= 0) { 31 | return 32 | } 33 | fileList.registerMultiSelectFileAction({ 34 | name: 'open-project', 35 | displayName: t('integration_openproject', 'Link to work package'), 36 | mime: 'all', 37 | permissions: OC.PERMISSION_READ, 38 | iconClass: 'icon-openproject', 39 | action: (selectedFiles) => { this.signExample(selectedFiles) }, 40 | }) 41 | }, 42 | signExample: (selectedFiles) => { 43 | // store all the file-id in an array and set the file ids 44 | const fileInfos = [] 45 | for (const file of selectedFiles) { 46 | const fileInfo = { 47 | id: file.id, 48 | name: file.name, 49 | } 50 | fileInfos.push(fileInfo) 51 | } 52 | OCA.OpenProject.LinkMultipleFilesModalVue.$children[0].setFileInfos(fileInfos) 53 | OCA.OpenProject.LinkMultipleFilesModalVue.$children[0].showModal() 54 | }, 55 | } 56 | })() 57 | 58 | OC.Plugins.register('OCA.Files.FileList', OCA.OpenProject.FilesPlugin) 59 | 60 | const modalId = 'multipleFileLinkModal' 61 | const modalElement = document.createElement('div') 62 | modalElement.id = modalId 63 | document.body.append(modalElement) 64 | 65 | OCA.OpenProject.LinkMultipleFilesModalVue = new Vue({ 66 | el: modalElement, 67 | render: h => { 68 | return h(LinkMultipleFilesModal) 69 | }, 70 | }) 71 | -------------------------------------------------------------------------------- /src/personalSettings.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion: 6 */ 2 | 3 | /** 4 | * SPDX-FileCopyrightText: 2021-2023 Nextcloud GmbH and Nextcloud contributors 5 | * SPDX-License-Identifier: AGPL-3.0-or-later 6 | */ 7 | 8 | import Vue from 'vue' 9 | import './bootstrap.js' 10 | import PersonalSettings from './components/PersonalSettings.vue' 11 | 12 | // eslint-disable-next-line 13 | 'use strict' 14 | 15 | // eslint-disable-next-line 16 | new Vue({ 17 | el: '#openproject_prefs', 18 | render: h => h(PersonalSettings), 19 | }) 20 | -------------------------------------------------------------------------------- /src/projectTab.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-FileCopyrightText: 2021-2022 Jankari Tech Pvt. Ltd. 4 | * SPDX-License-Identifier: AGPL-3.0-or-later 5 | */ 6 | 7 | import Vue from 'vue' 8 | 9 | import './bootstrap.js' 10 | import ProjectsTab from './views/ProjectsTab.vue' 11 | 12 | // Init OpenProject Tab Service 13 | if (!window.OCA.OpenProject) { 14 | window.OCA.OpenProject = {} 15 | } 16 | 17 | const View = Vue.extend(ProjectsTab) 18 | let TabInstance = null 19 | 20 | const projectTab = new OCA.Files.Sidebar.Tab({ 21 | id: 'open-project', 22 | name: t('integration_openproject', 'OpenProject'), 23 | icon: 'icon-openproject', 24 | 25 | async mount(el, fileInfo, context) { 26 | if (TabInstance) { 27 | TabInstance.$destroy() 28 | } 29 | TabInstance = new View({ 30 | // Better integration with vue parent component 31 | parent: context, 32 | }) 33 | // Only mount after we have all the info we need 34 | await TabInstance.update(fileInfo) 35 | TabInstance.$mount(el) 36 | }, 37 | update(fileInfo) { 38 | TabInstance.update(fileInfo) 39 | }, 40 | destroy() { 41 | TabInstance.$destroy() 42 | TabInstance = null 43 | }, 44 | }) 45 | 46 | window.addEventListener('DOMContentLoaded', function() { 47 | if (OCA.Files && OCA.Files.Sidebar) { 48 | OCA.Files.Sidebar.registerTab(projectTab) 49 | } 50 | }) 51 | -------------------------------------------------------------------------------- /src/reference.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023-2024 Jankari Tech Pvt. Ltd. 3 | * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors 4 | * SPDX-License-Identifier: AGPL-3.0-or-later 5 | */ 6 | 7 | // this requires @nextcloud/vue >= 7.9.0 8 | import { registerWidget, registerCustomPickerElement, NcCustomPickerRenderResult } from '@nextcloud/vue' 9 | 10 | // this is required for lazy loading 11 | __webpack_nonce__ = btoa(OC.requestToken) // eslint-disable-line 12 | __webpack_public_path__ = OC.linkTo('integration_openproject', 'js/') // eslint-disable-line 13 | 14 | // this is where we associate our widget component with the richobjects that we return in the reference provider 15 | registerWidget('integration_openproject_work_package', async (el, { richObjectType, richObject, accessible }) => { 16 | // here we lazy load the components so it does not slow down the initial page load 17 | const { default: Vue } = await import(/* webpackChunkName: "reference-wp-lazy" */'vue') 18 | const { default: WorkPackageReferenceWidget } = await import(/* webpackChunkName: "reference-wp-lazy" */'./views/WorkPackageReferenceWidget.vue') 19 | Vue.mixin({ methods: { t, n } }) 20 | const Widget = Vue.extend(WorkPackageReferenceWidget) 21 | new Widget({ 22 | propsData: { 23 | richObjectType, 24 | richObject, 25 | accessible, 26 | }, 27 | }).$mount(el) 28 | }) 29 | 30 | registerCustomPickerElement('openproject-work-package-ref', async (el, { providerId, accessible }) => { 31 | const { default: Vue } = await import(/* webpackChunkName: "reference-picker-lazy" */'vue') 32 | const { default: WorkPackagePickerElement } = await import(/* webpackChunkName: "reference-picker-lazy" */'./views/WorkPackagePickerElement.vue') 33 | Vue.mixin({ methods: { t, n } }) 34 | 35 | const Element = Vue.extend(WorkPackagePickerElement) 36 | const vueElement = new Element({ 37 | propsData: { 38 | providerId, 39 | accessible, 40 | }, 41 | }).$mount(el) 42 | return new NcCustomPickerRenderResult(vueElement.$el, vueElement) 43 | }, (el, renderResult) => { 44 | renderResult.object.$destroy() 45 | }) 46 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2021-2022 Jankari Tech Pvt. Ltd. 3 | * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors 4 | * SPDX-License-Identifier: AGPL-3.0-or-later 5 | */ 6 | 7 | const stylelintConfig = require('@nextcloud/stylelint-config') 8 | stylelintConfig.rules = { 9 | ...stylelintConfig.rules, 10 | 'declaration-colon-space-after': 'always', 11 | 'max-empty-lines': 1, 12 | "block-opening-brace-space-before": "always" 13 | } 14 | module.exports = stylelintConfig 15 | -------------------------------------------------------------------------------- /templates/adminSettings.php: -------------------------------------------------------------------------------- 1 | 11 | 12 |
-------------------------------------------------------------------------------- /templates/personalSettings.php: -------------------------------------------------------------------------------- 1 | 11 | 12 |
-------------------------------------------------------------------------------- /tests/acceptance/config/behat.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022-2024 Jankari Tech Pvt. Ltd. 2 | # SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | default: 5 | autoload: 6 | '': '%paths.base%/../features/bootstrap' 7 | suites: 8 | api: 9 | paths: 10 | - '%paths.base%/../features/api' 11 | contexts: 12 | - FeatureContext: 13 | baseUrl: http://localhost 14 | adminUsername: admin 15 | adminPassword: admin 16 | regularUserPassword: 123456 17 | - SharingContext: 18 | - DirectUploadContext: 19 | - GroupfoldersContext: 20 | - FilesVersionsContext: 21 | -------------------------------------------------------------------------------- /tests/acceptance/features/api/capabilities.feature: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-FileCopyrightText: 2023 Jankari Tech Pvt. Ltd. 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | Feature: get capabilities of the app 5 | 6 | Scenario: Get capabilities when team folder app is enabled 7 | Given user "Carol" has been created 8 | When the administrator requests the nextcloud capabilities 9 | Then the HTTP status code should be "200" 10 | And the ocs data of the response should match 11 | """" 12 | { 13 | "type": "object", 14 | "required": [ 15 | "capabilities" 16 | ], 17 | "properties": { 18 | "capabilities": { 19 | "type": "object", 20 | "required": [ 21 | "integration_openproject" 22 | ], 23 | "properties": { 24 | "integration_openproject": { 25 | "type": "object", 26 | "required": [ 27 | "app_version", 28 | "groupfolder_version", 29 | "groupfolders_enabled" 30 | ], 31 | "properties": { 32 | "app_version": { 33 | "type": "string", 34 | "pattern": "^\\d+\\.\\d+\\.\\d+(-\\w+.\\d+)?$" 35 | }, 36 | "groupfolder_version": { 37 | "type": "string", 38 | "pattern": "^\\d+\\.\\d+\\.\\d+(?:-\\w+.\\d+)?$" 39 | }, 40 | "groupfolders_enabled": { 41 | "type": "boolean", 42 | "enum": [ 43 | true 44 | ] 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | """ 53 | -------------------------------------------------------------------------------- /tests/acceptance/features/searchBar.feature: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Jankari Tech Pvt. Ltd. 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | Feature: list work-packages in the side bar 4 | As a Nextcloud user 5 | I want see all work packages on OpenProject that are linked to a file 6 | So that I can link the work-packages with a file 7 | 8 | Background: 9 | Given user "Alice" has been created 10 | And user "Alice" has uploaded file with content "some data" to "/file.txt" 11 | And user "Alice" has logged in using the webUI 12 | 13 | Scenario: search for a work package to a file 14 | Given the user has opened "openProject" section of file "/file.txt" using the webUI 15 | When the user searches for work package with name "This is a " using the searchbar 16 | Then all the the work packages with subject "This is a " should be displayed on the webUI 17 | 18 | Scenario: link a work package to a file 19 | Given the user has opened "openProject" section of file "/file.txt" using the webUI 20 | And the user has searched for work package with name "This is a " using the searchbar 21 | When the user links the work package "This is a work package" using the webUI 22 | Then the work package "This is a work package" should be listed on the webUI 23 | 24 | Scenario: link multiple work packages to a file 25 | Given the user has opened "openProject" section of file "/file.txt" using the webUI 26 | When the user searches for work package with name "This is a work package" using the searchbar 27 | And the user links the work package "This is a work package" using the webUI 28 | And the user searches for work package with name "This is second work package" using the searchbar 29 | And the user links the work package "This is second work package" using the webUI 30 | And the user searches for work package with name "This is third work package" using the searchbar 31 | And the user links the work package "This is third work package" using the webUI 32 | Then the following work packages should be listed on the webUI: 33 | | work-packages | 34 | | This is a work package | 35 | | This is second work package | 36 | | This is third work package | 37 | -------------------------------------------------------------------------------- /tests/jest/__mocks__/@nextcloud/l10n.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion: 8 */ 2 | /** 3 | * SPDX-FileCopyrightText: 2022 Jankari Tech Pvt. Ltd. 4 | * SPDX-License-Identifier: AGPL-3.0-or-later 5 | */ 6 | 7 | const l10n = jest.createMockFromModule('@nextcloud/l10n') 8 | 9 | l10n.translate = jest.fn(function(app, msg) { 10 | return msg 11 | }) 12 | 13 | module.exports = l10n 14 | -------------------------------------------------------------------------------- /tests/jest/__mocks__/@nextcloud/router.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion: 8 */ 2 | /** 3 | * SPDX-FileCopyrightText: 2021-2022 Jankari Tech Pvt. Ltd. 4 | * SPDX-License-Identifier: AGPL-3.0-or-later 5 | */ 6 | 7 | const router = jest.createMockFromModule('@nextcloud/router') 8 | 9 | router.generateUrl = jest.fn(function(url) { 10 | return 'http://localhost' + url 11 | }) 12 | 13 | module.exports = router 14 | -------------------------------------------------------------------------------- /tests/jest/components/__snapshots__/OAuthConnectButton.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`OAuthConnectButton.vue when the admin config is not okay should show message for admin user 1`] = ` 4 | VueWrapper { 5 | "_emitted": {}, 6 | "_emittedByOrder": [], 7 | "isFunctionalComponent": undefined, 8 | } 9 | `; 10 | 11 | exports[`OAuthConnectButton.vue when the admin config is not okay should show message for normal user 1`] = ` 12 | VueWrapper { 13 | "_emitted": {}, 14 | "_emittedByOrder": [], 15 | "isFunctionalComponent": undefined, 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /tests/jest/components/admin/FormHeading.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2022-2023 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-FileCopyrightText: 2022 Jankari Tech Pvt. Ltd. 4 | * SPDX-License-Identifier: AGPL-3.0-or-later 5 | */ 6 | 7 | import { createLocalVue, mount } from '@vue/test-utils' 8 | import FormHeading from '../../../../src/components/admin/FormHeading.vue' 9 | 10 | const localVue = createLocalVue() 11 | 12 | global.t = (app, text) => text 13 | 14 | describe('FormHeading.vue', () => { 15 | describe('is complete prop', () => { 16 | it('should show checkmark icon, add green title and hide the index if complete', () => { 17 | const wrapper = getWrapper({ 18 | isComplete: true, 19 | }) 20 | expect(wrapper).toMatchSnapshot() 21 | }) 22 | it('should hide the checkmark icon and show the index if not complete', () => { 23 | const wrapper = getWrapper({ 24 | isComplete: false, 25 | }) 26 | expect(wrapper).toMatchSnapshot() 27 | }) 28 | }) 29 | describe('is disabled prop', () => { 30 | it('should add disabled class to the form heading', () => { 31 | const wrapper = getWrapper({ 32 | isDisabled: true, 33 | }) 34 | expect(wrapper).toMatchSnapshot() 35 | }) 36 | }) 37 | }) 38 | 39 | function getWrapper(props = {}) { 40 | return mount(FormHeading, { 41 | localVue, 42 | propsData: { 43 | title: 'Some Field Title', 44 | index: '1', 45 | ...props, 46 | }, 47 | mocks: { 48 | t: (app, msg) => msg, 49 | }, 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /tests/jest/components/admin/__snapshots__/FieldValue.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`FieldValue.vue encrypt value prop should render the actual value as it is if not set 1`] = ` 4 | Wrapper { 5 | "selector": ".field-item-value", 6 | } 7 | `; 8 | 9 | exports[`FieldValue.vue encrypt value prop when set as 'true' should render the encrypted value 1`] = ` 10 | Wrapper { 11 | "selector": ".field-item-value", 12 | } 13 | `; 14 | 15 | exports[`FieldValue.vue encrypt value prop when set as 'true' with inspection prop should show the inspect button with the eye icon if set 1`] = ` 16 | VueWrapper { 17 | "_emitted": {}, 18 | "_emittedByOrder": [], 19 | "isFunctionalComponent": undefined, 20 | } 21 | `; 22 | 23 | exports[`FieldValue.vue encrypt value prop when set as 'true' with inspection prop should toggle encrypted value when the inspect button is clicked 1`] = ` 24 | Wrapper { 25 | "selector": ".field-item-value", 26 | } 27 | `; 28 | 29 | exports[`FieldValue.vue encrypt value prop when set as 'true' with inspection prop should toggle encrypted value when the inspect button is clicked 2`] = ` 30 | Wrapper { 31 | "selector": ".field-item-value", 32 | } 33 | `; 34 | 35 | exports[`FieldValue.vue encrypt value prop when set as 'true' with inspection prop should toggle the inspect button icon when the inspect button is clicked 1`] = ` 36 | VueWrapper { 37 | "_emitted": {}, 38 | "_emittedByOrder": [], 39 | "isFunctionalComponent": undefined, 40 | "selector": ".eye-off-icon", 41 | } 42 | `; 43 | 44 | exports[`FieldValue.vue encrypt value prop when set as 'true' with inspection prop should toggle the inspect button icon when the inspect button is clicked 2`] = ` 45 | VueWrapper { 46 | "_emitted": {}, 47 | "_emittedByOrder": [], 48 | "isFunctionalComponent": undefined, 49 | "selector": ".eye-icon", 50 | } 51 | `; 52 | 53 | exports[`FieldValue.vue is required prop should append asterik with the title if set 1`] = ` 54 | VueWrapper { 55 | "_emitted": {}, 56 | "_emittedByOrder": [], 57 | "isFunctionalComponent": undefined, 58 | } 59 | `; 60 | 61 | exports[`FieldValue.vue is required prop should not append asterik with the title if not set 1`] = ` 62 | VueWrapper { 63 | "_emitted": {}, 64 | "_emittedByOrder": [], 65 | "isFunctionalComponent": undefined, 66 | } 67 | `; 68 | -------------------------------------------------------------------------------- /tests/jest/components/admin/__snapshots__/FormHeading.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`FormHeading.vue is complete prop should hide the checkmark icon and show the index if not complete 1`] = ` 4 | VueWrapper { 5 | "_emitted": {}, 6 | "_emittedByOrder": [], 7 | "isFunctionalComponent": undefined, 8 | } 9 | `; 10 | 11 | exports[`FormHeading.vue is complete prop should show checkmark icon, add green title and hide the index if complete 1`] = ` 12 | VueWrapper { 13 | "_emitted": {}, 14 | "_emittedByOrder": [], 15 | "isFunctionalComponent": undefined, 16 | } 17 | `; 18 | 19 | exports[`FormHeading.vue is disabled prop should add disabled class to the form heading 1`] = ` 20 | VueWrapper { 21 | "_emitted": {}, 22 | "_emittedByOrder": [], 23 | "isFunctionalComponent": undefined, 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /tests/jest/components/admin/__snapshots__/TextInput.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TextInput.vue is required prop should add asterik to the label text 1`] = ` 4 | Wrapper { 5 | "selector": ".text-input-label", 6 | } 7 | `; 8 | 9 | exports[`TextInput.vue is required prop should not add asterik to the label text 1`] = ` 10 | Wrapper { 11 | "selector": ".text-input-label", 12 | } 13 | `; 14 | 15 | exports[`TextInput.vue messages should not show error message details if not error message is provided 1`] = ` 16 | VueWrapper { 17 | "_emitted": {}, 18 | "_emittedByOrder": [], 19 | "isFunctionalComponent": undefined, 20 | } 21 | `; 22 | 23 | exports[`TextInput.vue messages should show error message details if both error message and details are provided 1`] = ` 24 | VueWrapper { 25 | "_emitted": {}, 26 | "_emittedByOrder": [], 27 | "isFunctionalComponent": undefined, 28 | } 29 | `; 30 | 31 | exports[`TextInput.vue messages should show error message if both error message and hint text are provided 1`] = ` 32 | VueWrapper { 33 | "_emitted": {}, 34 | "_emittedByOrder": [], 35 | "isFunctionalComponent": undefined, 36 | } 37 | `; 38 | 39 | exports[`TextInput.vue messages should show error message if provided 1`] = ` 40 | VueWrapper { 41 | "_emitted": {}, 42 | "_emittedByOrder": [], 43 | "isFunctionalComponent": undefined, 44 | } 45 | `; 46 | 47 | exports[`TextInput.vue messages should show hint text if provided 1`] = ` 48 | VueWrapper { 49 | "_emitted": {}, 50 | "_emittedByOrder": [], 51 | "isFunctionalComponent": undefined, 52 | } 53 | `; 54 | 55 | exports[`TextInput.vue readonly prop should set the input to readonly 1`] = ` 56 | VueWrapper { 57 | "_emitted": {}, 58 | "_emittedByOrder": [], 59 | "isFunctionalComponent": undefined, 60 | } 61 | `; 62 | 63 | exports[`TextInput.vue with copy button prop should render copy button if set 1`] = ` 64 | VueWrapper { 65 | "_emitted": {}, 66 | "_emittedByOrder": [], 67 | "isFunctionalComponent": undefined, 68 | } 69 | `; 70 | -------------------------------------------------------------------------------- /tests/jest/components/settings/ErrorNote.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2025 Jankari Tech Pvt. Ltd. 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { mount } from '@vue/test-utils' 7 | 8 | import ErrorNote from '../../../../src/components/settings/ErrorNote.vue' 9 | 10 | describe('Component: ErrorNote', () => { 11 | it('should show error title', () => { 12 | const wrapper = getWrapper({ errorTitle: 'Test error' }) 13 | 14 | expect(wrapper.element).toMatchSnapshot() 15 | }) 16 | it('should show error title and error message', () => { 17 | const wrapper = getWrapper({ errorTitle: 'Test error', errorMessage: 'Test error message' }) 18 | 19 | expect(wrapper.element).toMatchSnapshot() 20 | }) 21 | it('should show all provided props', () => { 22 | const wrapper = getWrapper({ 23 | errorTitle: 'Test error', 24 | errorMessage: 'Test error message', 25 | errorLink: 'http://example.com', 26 | errorLinkLabel: 'Test link', 27 | }) 28 | 29 | expect(wrapper.element).toMatchSnapshot() 30 | }) 31 | }) 32 | 33 | function getWrapper(propsData = {}) { 34 | return mount(ErrorNote, { 35 | propsData, 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /tests/jest/components/tab/EmptyContent.spec.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion: 8 */ 2 | 3 | /** 4 | * SPDX-FileCopyrightText: 2022-2025 Jankari Tech Pvt. Ltd. 5 | * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors 6 | * SPDX-License-Identifier: AGPL-3.0-or-later 7 | */ 8 | 9 | import { shallowMount, createLocalVue } from '@vue/test-utils' 10 | import EmptyContent from '../../../../src/components/tab/EmptyContent.vue' 11 | import { AUTH_METHOD, STATE } from '../../../../src/utils.js' 12 | const localVue = createLocalVue() 13 | 14 | describe('EmptyContent.vue', () => { 15 | let wrapper 16 | const emptyContentMessageSelector = '.empty-content--message' 17 | const connectButtonSelector = 'oauthconnectbutton-stub' 18 | 19 | describe('connect button', () => { 20 | it.each([{ 21 | state: STATE.OK, 22 | viewed: false, 23 | }, { 24 | state: STATE.NO_TOKEN, 25 | viewed: true, 26 | }, { 27 | state: STATE.ERROR, 28 | viewed: true, 29 | }, { 30 | state: STATE.CONNECTION_ERROR, 31 | viewed: false, 32 | }])('should be displayed depending on the state', (cases) => { 33 | wrapper = getWrapper({ state: cases.state }) 34 | expect(wrapper.find(connectButtonSelector).exists()).toBe(cases.viewed) 35 | }) 36 | }) 37 | describe('content title', () => { 38 | it('should not be displayed if the admin config is not okay', () => { 39 | wrapper = getWrapper({ isAdminConfigOk: false }) 40 | expect(wrapper.find(emptyContentMessageSelector).exists()).toBe(false) 41 | }) 42 | it.each([ 43 | STATE.NO_TOKEN, 44 | STATE.ERROR, 45 | STATE.CONNECTION_ERROR, 46 | STATE.FAILED_FETCHING_WORKPACKAGES, 47 | STATE.OK, 48 | ])('shows the correct empty message depending on states if the admin config is okay', async (state) => { 49 | wrapper = getWrapper({ state, adminConfigStatus: true }) 50 | expect(wrapper.find(emptyContentMessageSelector).exists()).toBeTruthy() 51 | expect(wrapper.find(emptyContentMessageSelector)).toMatchSnapshot() 52 | }) 53 | }) 54 | }) 55 | 56 | function getWrapper(propsData = {}) { 57 | return shallowMount(EmptyContent, { 58 | localVue, 59 | mocks: { 60 | t: (msg) => msg, 61 | }, 62 | propsData: { 63 | state: 'ok', 64 | isAdminConfigOk: true, 65 | authMethod: AUTH_METHOD.OAUTH2, 66 | ...propsData, 67 | }, 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /tests/jest/components/tab/WorkPackage.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-FileCopyrightText: 2022 Jankari Tech Pvt. Ltd. 4 | * SPDX-License-Identifier: AGPL-3.0-or-later 5 | */ 6 | 7 | import { createLocalVue, shallowMount } from '@vue/test-utils' 8 | 9 | import WorkPackage from '../../../../src/components/tab/WorkPackage.vue' 10 | import workPackagesSearchResponse from '../../fixtures/workPackagesSearchResponse.json' 11 | 12 | const localVue = createLocalVue() 13 | 14 | describe('WorkPackage.vue', () => { 15 | let wrapper 16 | const workPackagesSelector = '.workpackage' 17 | beforeEach(() => { 18 | 19 | wrapper = shallowMount(WorkPackage, { 20 | localVue, 21 | propsData: { 22 | workpackage: workPackagesSearchResponse[0], 23 | }, 24 | }) 25 | }) 26 | it('shows work packages information', async () => { 27 | const workPackages = wrapper.find(workPackagesSelector) 28 | expect(workPackages.exists()).toBeTruthy() 29 | expect(workPackages).toMatchSnapshot() 30 | 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /tests/jest/components/tab/__snapshots__/EmptyContent.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`EmptyContent.vue content title shows the correct empty message depending on states if the admin config is okay 1`] = ` 4 | Wrapper { 5 | "selector": ".empty-content--message", 6 | } 7 | `; 8 | 9 | exports[`EmptyContent.vue content title shows the correct empty message depending on states if the admin config is okay 2`] = ` 10 | Wrapper { 11 | "selector": ".empty-content--message", 12 | } 13 | `; 14 | 15 | exports[`EmptyContent.vue content title shows the correct empty message depending on states if the admin config is okay 3`] = ` 16 | Wrapper { 17 | "selector": ".empty-content--message", 18 | } 19 | `; 20 | 21 | exports[`EmptyContent.vue content title shows the correct empty message depending on states if the admin config is okay 4`] = ` 22 | Wrapper { 23 | "selector": ".empty-content--message", 24 | } 25 | `; 26 | 27 | exports[`EmptyContent.vue content title shows the correct empty message depending on states if the admin config is okay 5`] = ` 28 | Wrapper { 29 | "selector": ".empty-content--message", 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /tests/jest/components/tab/__snapshots__/SearchInput.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SearchInput.vue state messages any: should display the correct state message 1`] = ` 4 | Wrapper { 5 | "selector": ".stateMsg", 6 | } 7 | `; 8 | 9 | exports[`SearchInput.vue state messages error: should display the correct state message 1`] = ` 10 | Wrapper { 11 | "selector": ".stateMsg", 12 | } 13 | `; 14 | 15 | exports[`SearchInput.vue state messages no-token: should display the correct state message 1`] = ` 16 | Wrapper { 17 | "selector": ".stateMsg", 18 | } 19 | `; 20 | 21 | exports[`SearchInput.vue work packages select search list should display correct options list of search results 1`] = ` 22 | { 23 | "isLinkPreviews": false, 24 | "isSmartPicker": false, 25 | "workpackage": { 26 | "assignee": "test", 27 | "fileId": 1234, 28 | "id": "1", 29 | "picture": "/server/index.php/apps/integration_openproject/avatar?userId=1&userName=System", 30 | "project": "test", 31 | "projectId": "15", 32 | "statusCol": "blue", 33 | "statusTitle": "in-progress", 34 | "subject": "Organize work-packages", 35 | "typeCol": "red", 36 | "typeTitle": "task", 37 | }, 38 | } 39 | `; 40 | 41 | exports[`SearchInput.vue work packages select search list should not be displayed if the search results is empty 1`] = ` 42 | Wrapper { 43 | "selector": "[role="listbox"]", 44 | } 45 | `; 46 | 47 | exports[`SearchInput.vue work packages select search list should only use the options from the latest search response 1`] = ` 48 | { 49 | "isLinkPreviews": false, 50 | "isSmartPicker": false, 51 | "workpackage": { 52 | "assignee": "some assignee", 53 | "fileId": 111, 54 | "id": 1, 55 | "picture": "http://nc.local/apps/integration_openproject/api/v1/avatar?userId=&userName=some%20assignee", 56 | "project": "some project", 57 | "projectId": "", 58 | "statusCol": "", 59 | "statusTitle": "some status", 60 | "subject": "some subject", 61 | "typeCol": "", 62 | "typeTitle": "some type", 63 | }, 64 | } 65 | `; 66 | -------------------------------------------------------------------------------- /tests/jest/components/tab/__snapshots__/WorkPackage.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`WorkPackage.vue shows work packages information 1`] = ` 4 | VueWrapper { 5 | "_emitted": {}, 6 | "_emittedByOrder": [], 7 | "isFunctionalComponent": undefined, 8 | "selector": ".workpackage", 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /tests/jest/fixtures/openprojectAvailableProjectResponseAfterSearch.json: -------------------------------------------------------------------------------- 1 | { 2 | "12": { 3 | "_type": "Project", 4 | "id": 12, 5 | "identifier": "searchedProject", 6 | "name": "searchedProject", 7 | "active": true, 8 | "public": false, 9 | "description": { 10 | "format": "markdown", 11 | "raw": "", 12 | "html": "" 13 | }, 14 | "createdAt": "2023-10-10T11:20:55Z", 15 | "updatedAt": "2023-10-10T11:20:55Z", 16 | "statusExplanation": { 17 | "format": "markdown", 18 | "raw": "", 19 | "html": "" 20 | }, 21 | "_links": { 22 | "self": { 23 | "href": "/api/v3/projects/12", 24 | "title": "searchedProject" 25 | }, 26 | "createWorkPackage": { 27 | "href": "/api/v3/projects/12/work_packages/form", 28 | "method": "post" 29 | }, 30 | "createWorkPackageImmediately": { 31 | "href": "/api/v3/projects/12/work_packages", 32 | "method": "post" 33 | }, 34 | "workPackages": { 35 | "href": "/api/v3/projects/12/work_packages" 36 | }, 37 | "categories": { 38 | "href": "/api/v3/projects/12/categories" 39 | }, 40 | "versions": { 41 | "href": "/api/v3/projects/12/versions" 42 | }, 43 | "memberships": { 44 | "href": "/api/v3/memberships?filters=%5B%7B%22project%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%227%22%5D%7D%7D%5D" 45 | }, 46 | "types": { 47 | "href": "/api/v3/projects/12/types" 48 | }, 49 | "update": { 50 | "href": "/api/v3/projects/12/form", 51 | "method": "post" 52 | }, 53 | "updateImmediately": { 54 | "href": "/api/v3/projects/12", 55 | "method": "patch" 56 | }, 57 | "delete": { 58 | "href": "/api/v3/projects/12", 59 | "method": "delete" 60 | }, 61 | "schema": { 62 | "href": "/api/v3/projects/schema" 63 | }, 64 | "status": { 65 | "href": null 66 | }, 67 | "projectStorages": { 68 | "href": "/api/v3/project_storages?filters=%5B%7B%22projectId%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%227%22%5D%7D%7D%5D" 69 | }, 70 | "parent": { 71 | "href": "/api/v3/projects/5", 72 | "title": "[dev] Large child" 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/jest/fixtures/workPackagesSearchResponse.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fileId": 1234, 4 | "id" : "1", 5 | "subject" : "Organize work-packages", 6 | "project" : "test", 7 | "projectId" : "15", 8 | "statusTitle" :"in-progress", 9 | "typeTitle" : "task", 10 | "assignee" : "test", 11 | "statusCol" : "blue", 12 | "typeCol" : "red", 13 | "picture" : "/server/index.php/apps/integration_openproject/avatar?userId=1&userName=System" 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /tests/jest/fixtures/workPackagesSearchResponseNoAssignee.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "1", 4 | "subject": "Organize work-packages", 5 | "project": "test", 6 | "projectId" : "15", 7 | "statusTitle": "in-progress", 8 | "typeTitle": "task", 9 | "assignee": "", 10 | "statusCol": "blue", 11 | "typeCol": "red", 12 | "picture": "/server/index.php/apps/integration_openproject/avatar?userId=1&userName=System" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /tests/jest/global.mock.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Jankari Tech Pvt. Ltd. 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | global.structuredClone = v => JSON.parse(JSON.stringify(v)) 7 | -------------------------------------------------------------------------------- /tests/jest/stubs/empty.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2021 Jankari Tech Pvt. Ltd. 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | export default {} 7 | -------------------------------------------------------------------------------- /tests/jest/views/__snapshots__/CreateWorkpackageModal.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CreateWorkPackageModal.vue workpackage creation form should display available projects in the project dropdown 1`] = ` 4 | VueWrapper { 5 | "_emitted": {}, 6 | "_emittedByOrder": [], 7 | "isFunctionalComponent": undefined, 8 | "selector": "[data-test-id="available-projects"]", 9 | } 10 | `; 11 | 12 | exports[`CreateWorkPackageModal.vue workpackage creation form should send the form validation request when type is changed 1`] = ` 13 | VueWrapper { 14 | "_emitted": {}, 15 | "_emittedByOrder": [], 16 | "isFunctionalComponent": undefined, 17 | "selector": "[data-test-id="available-statuses"]", 18 | } 19 | `; 20 | 21 | exports[`CreateWorkPackageModal.vue workpackage creation form should set the available types, status and assignee when a project is selected 1`] = ` 22 | VueWrapper { 23 | "_emitted": {}, 24 | "_emittedByOrder": [], 25 | "isFunctionalComponent": undefined, 26 | "selector": "[data-test-id="available-types"]", 27 | } 28 | `; 29 | 30 | exports[`CreateWorkPackageModal.vue workpackage creation form should set the available types, status and assignee when a project is selected 2`] = ` 31 | VueWrapper { 32 | "_emitted": {}, 33 | "_emittedByOrder": [], 34 | "isFunctionalComponent": undefined, 35 | "selector": "[data-test-id="available-statuses"]", 36 | } 37 | `; 38 | 39 | exports[`CreateWorkPackageModal.vue workpackage creation form should set the available types, status and assignee when a project is selected 3`] = ` 40 | VueWrapper { 41 | "_emitted": {}, 42 | "_emittedByOrder": [], 43 | "isFunctionalComponent": undefined, 44 | "selector": "[data-test-id="available-assignees"]", 45 | } 46 | `; 47 | -------------------------------------------------------------------------------- /tests/jest/views/__snapshots__/LinkMultipleFilesModal.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`LinkMultipleFilesModal.vue fetch workpackages multiple files selected sets the "error" state if the admin config is not okay 1`] = ` 4 | VueWrapper { 5 | "_emitted": {}, 6 | "_emittedByOrder": [], 7 | "isFunctionalComponent": undefined, 8 | } 9 | `; 10 | 11 | exports[`LinkMultipleFilesModal.vue fetch workpackages single file selected sets the "error" state if the admin config is not okay 1`] = ` 12 | VueWrapper { 13 | "_emitted": {}, 14 | "_emittedByOrder": [], 15 | "isFunctionalComponent": undefined, 16 | } 17 | `; 18 | 19 | exports[`LinkMultipleFilesModal.vue fetch workpackages single file selected sets the "error" state if the admin config is not okay 2`] = ` 20 | VueWrapper { 21 | "_emitted": {}, 22 | "_emittedByOrder": [], 23 | "isFunctionalComponent": undefined, 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /tests/jest/views/__snapshots__/ProjectsTab.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ProjectsTab.vue fetchWorkpackages adds every work-package only once 1`] = ` 4 | Wrapper { 5 | "selector": "#openproject-linked-workpackages", 6 | } 7 | `; 8 | 9 | exports[`ProjectsTab.vue fetchWorkpackages sets the "error" state if the admin config is not okay 1`] = ` 10 | VueWrapper { 11 | "_emitted": {}, 12 | "_emittedByOrder": [], 13 | "isFunctionalComponent": undefined, 14 | } 15 | `; 16 | 17 | exports[`ProjectsTab.vue fetchWorkpackages shows the linked workpackages 1`] = ` 18 | Wrapper { 19 | "selector": "#openproject-linked-workpackages", 20 | } 21 | `; 22 | 23 | exports[`ProjectsTab.vue fetchWorkpackages shows the linked workpackages 2`] = ` 24 | Wrapper { 25 | "selector": "#openproject-linked-workpackages", 26 | } 27 | `; 28 | 29 | exports[`ProjectsTab.vue onSave shows the just linked workpackage 1`] = ` 30 | Wrapper { 31 | "selector": "#openproject-linked-workpackages", 32 | } 33 | `; 34 | -------------------------------------------------------------------------------- /tests/lib/Service/DirectUploadServiceTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(DatabaseService::class)->disableOriginalConstructor()->getMock(); 20 | $databaseServiceMock->method('getTokenInfoFromDB')->willReturn([ 21 | 'user_id' => 'testUser', 22 | 'expires_on' => $expiresOn, 23 | 'folder_id' => '123' 24 | ]); 25 | 26 | $userMock = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock(); 27 | $userMock->method('isEnabled')->willReturn(true); 28 | 29 | $userManagerMock = $this->getMockBuilder(IUserManager::class)->disableOriginalConstructor()->getMock(); 30 | $userManagerMock->method('userExists')->willReturn(true); 31 | $userManagerMock->method('get')->willReturn($userMock); 32 | 33 | return new DirectUploadService( 34 | $userManagerMock, 35 | $this->createMock(IL10N::class), 36 | $this->createMock(ISecureRandom::class), 37 | $databaseServiceMock 38 | ); 39 | } 40 | 41 | 42 | /** 43 | * @return void 44 | * 45 | */ 46 | public function testGetTokenInfoExpiredTime() { 47 | //send an already expired time 48 | $direcUploadService = $this->getDirectUploadService(1671537979); 49 | $this->expectException(NotFoundException::class); 50 | $direcUploadService->getTokenInfo('ziPpdeFW4qoTg7AzEc4E9bnREkF97f5B2q65M4t3iex58E7ENZK4GomwEZCPjeNa'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/lib/TokenEventFactoryTest.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public function settingsProvider(): array { 23 | return [ 24 | "Nextcloud Hub setup" => [ 25 | "providerType" => OpenProjectAPIService::NEXTCLOUD_HUB_PROVIDER, 26 | "tokenExchange" => false, 27 | "class" => InternalTokenRequestedEvent::class, 28 | ], 29 | "External IDP without token exchange" => [ 30 | "providerType" => "external", 31 | "tokenExchange" => false, 32 | "class" => ExternalTokenRequestedEvent::class, 33 | ], 34 | "External IDP with token exchange" => [ 35 | "providerType" => "external", 36 | "tokenExchange" => true, 37 | "class" => ExchangedTokenRequestedEvent::class, 38 | ], 39 | "Nextcloud Hub with token exchange enabled" => [ 40 | "providerType" => OpenProjectAPIService::NEXTCLOUD_HUB_PROVIDER, 41 | "tokenExchange" => true, 42 | "class" => InternalTokenRequestedEvent::class, 43 | ], 44 | ]; 45 | } 46 | 47 | /** 48 | * @dataProvider settingsProvider 49 | * @return void 50 | */ 51 | public function testGetEvent($providerType, $tokenExchange, $class): void { 52 | $configMock = $this->createMock(IConfig::class); 53 | $configMock->method("getAppValue")->willReturnMap([ 54 | [Application::APP_ID, "sso_provider_type", "", $providerType], 55 | [Application::APP_ID, "token_exchange", "", $tokenExchange], 56 | [Application::APP_ID, "targeted_audience_client_id", "", "test-client"], 57 | ]); 58 | 59 | $factory = new TokenEventFactory($configMock); 60 | $event = $factory->getEvent(); 61 | $this->assertInstanceOf($class, $event); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/lib/fixtures/openproject-icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextcloud/integration_openproject/be3412e2fae7bf9afe127358f3e6fcd3a478040f/tests/lib/fixtures/openproject-icon.jpg -------------------------------------------------------------------------------- /tests/lib/fixtures/openproject-icon.jpg.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2024 OpenProject GmbH 2 | SPDX-License-Identifier: LicenseRef-OpenProjectTrademarks 3 | -------------------------------------------------------------------------------- /tests/stub/doctrine_cacheItem.phpstub: -------------------------------------------------------------------------------- 1 | // CacheItem implementation of CacheItemInterface doesn't have the exact structure in terms of return type 2 | // and the psalm analyzer fails early with this error: 3 | // PHP Fatal error: Declaration of Doctrine\Common\Cache\Psr6\CacheItem::get() must be compatible 4 | // with Psr\Cache\CacheItemInterface::get(): mixed in nextcloud/master/3rdparty/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheItem.php on line 51 5 | // So the class CacheItem is stubbed 6 | 7 |