├── .dockerignore ├── .editorconfig ├── .envrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ └── config.yml ├── actions │ └── setup-frontend │ │ └── action.yml └── workflows │ ├── ci.yml │ ├── crowdin.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── build ├── after-install.sh └── reprepro-dist-conf ├── cliff.toml ├── code-header-template.txt ├── config-raw.json ├── contrib └── clean-translations.js ├── crowdin.yml ├── desktop ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.js ├── build │ ├── icon.icns │ └── icon.png ├── cliff.toml ├── main.js ├── package.json ├── pnpm-lock.yaml └── portInUse.js ├── devenv.lock ├── devenv.nix ├── devenv.yaml ├── frontend ├── .editorconfig ├── .env.local.example ├── .gitignore ├── .npmrc ├── .nvmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cliff.toml ├── cypress.config.ts ├── cypress │ ├── README.md │ ├── docker-compose.yml │ ├── e2e │ │ ├── misc │ │ │ └── menu.spec.ts │ │ ├── project │ │ │ ├── prepareProjects.ts │ │ │ ├── project-history.spec.ts │ │ │ ├── project-view-gantt.spec.ts │ │ │ ├── project-view-kanban.spec.ts │ │ │ ├── project-view-list.spec.ts │ │ │ ├── project-view-table.spec.ts │ │ │ └── project.spec.ts │ │ ├── sharing │ │ │ ├── linkShare.spec.ts │ │ │ └── team.spec.ts │ │ ├── task │ │ │ ├── overview.spec.ts │ │ │ └── task.spec.ts │ │ ├── tsconfig.json │ │ └── user │ │ │ ├── login.spec.ts │ │ │ ├── logout.spec.ts │ │ │ ├── password-reset.spec.ts │ │ │ ├── registration.spec.ts │ │ │ └── settings.spec.ts │ ├── factories │ │ ├── bucket.ts │ │ ├── label_task.ts │ │ ├── labels.ts │ │ ├── link_sharing.ts │ │ ├── project.ts │ │ ├── project_view.ts │ │ ├── task.ts │ │ ├── task_assignee.ts │ │ ├── task_attachments.ts │ │ ├── task_buckets.ts │ │ ├── task_comment.ts │ │ ├── task_reminders.ts │ │ ├── team.ts │ │ ├── team_member.ts │ │ ├── token.ts │ │ ├── user.ts │ │ └── users_project.ts │ ├── fixtures │ │ └── image.jpg │ └── support │ │ ├── authenticateUser.ts │ │ ├── commands.ts │ │ ├── component.index.html │ │ ├── component.ts │ │ ├── e2e.ts │ │ ├── factory.ts │ │ ├── index.d.ts │ │ ├── seed.ts │ │ └── updateUserSettings.ts ├── docs │ └── models-services.md ├── embed.go ├── env.config.d.ts ├── env.d.ts ├── eslint.config.js ├── histoire.config.ts ├── index.html ├── netlify.toml ├── originalMedia │ ├── audio │ │ ├── pop.mp3 │ │ └── pop.wav │ ├── fonts │ │ ├── OpenSans-Italic[wdth,wght].ttf │ │ ├── OpenSans[wdth,wght].ttf │ │ └── Quicksand[wght].ttf │ ├── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon.png │ │ ├── badge-monochrome.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── icon-maskable.png │ │ ├── msapplication-icon-144x144.png │ │ ├── mstile-150x150.png │ │ └── safari-pinned-tab.svg │ └── images │ │ ├── cool.svg │ │ ├── llama-nightscape.png │ │ ├── llama-nightscape.svg │ │ ├── llama.svg │ │ ├── logo-full-pride.svg │ │ ├── logo-full-white.svg │ │ ├── logo-full.svg │ │ ├── logo.svg │ │ └── migration │ │ ├── microsoft-todo.svg │ │ ├── todoist.svg │ │ ├── trello.svg │ │ └── wunderlist.png ├── package.json ├── patches │ ├── @github__hotkey@3.1.1.patch │ └── flexsearch@0.7.43.patch ├── pnpm-lock.yaml ├── public │ ├── emojis.json │ ├── favicon.ico │ ├── images │ │ └── icons │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon-120x120.png │ │ │ ├── apple-touch-icon-152x152.png │ │ │ ├── apple-touch-icon-180x180.png │ │ │ ├── apple-touch-icon-60x60.png │ │ │ ├── apple-touch-icon-76x76.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── badge-monochrome.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── icon-maskable.png │ │ │ ├── msapplication-icon-144x144.png │ │ │ ├── mstile-150x150.png │ │ │ └── safari-pinned-tab.svg │ └── robots.txt ├── scripts │ ├── deploy-preview-netlify.mjs │ ├── deploy-preview-netlify.mjs.sha384 │ ├── fonts-download.sh │ └── fonts-subset.sh ├── src │ ├── App.vue │ ├── assets │ │ ├── audio │ │ │ └── pop.mp3 │ │ ├── checkbox.svg │ │ ├── fonts │ │ │ ├── OpenSans-BoldItalic_3ff98862.woff2 │ │ │ ├── OpenSans-Bold_eb52363b.woff2 │ │ │ ├── OpenSans-Italic[wght]_c9a8fe68.woff2 │ │ │ ├── OpenSans-RegularItalic_48244a7a.woff2 │ │ │ ├── OpenSans-Regular_d0acb717.woff2 │ │ │ ├── OpenSans[wght]_54a65da5.woff2 │ │ │ ├── Quicksand-Bold_20b26f76.woff2 │ │ │ ├── Quicksand-Regular_3e913e7e.woff2 │ │ │ ├── Quicksand-SemiBold_be48a442.woff2 │ │ │ └── Quicksand[wght]_87bdcc7f.woff2 │ │ ├── llama-cool.svg │ │ ├── llama-nightscape.jpg │ │ ├── llama.svg │ │ ├── logo-full-pride.svg │ │ ├── logo-full.svg │ │ ├── logo.svg │ │ └── no-auth-image.jpg │ ├── components │ │ ├── base │ │ │ ├── BaseButton.story.vue │ │ │ ├── BaseButton.vue │ │ │ ├── BaseCheckbox.vue │ │ │ ├── BasePagination.vue │ │ │ └── Expandable.vue │ │ ├── date │ │ │ ├── DatemathHelp.story.vue │ │ │ ├── DatemathHelp.vue │ │ │ ├── DatepickerWithRange.vue │ │ │ ├── DatepickerWithValues.vue │ │ │ └── dateRanges.ts │ │ ├── home │ │ │ ├── AddToHomeScreen.vue │ │ │ ├── AppHeader.vue │ │ │ ├── ContentAuth.vue │ │ │ ├── ContentLinkShare.vue │ │ │ ├── DemoMode.vue │ │ │ ├── ImportHint.vue │ │ │ ├── Logo.vue │ │ │ ├── MenuButton.vue │ │ │ ├── Navigation.vue │ │ │ ├── PoweredByLink.vue │ │ │ ├── ProjectsNavigation.vue │ │ │ ├── ProjectsNavigationItem.vue │ │ │ └── UpdateNotification.vue │ │ ├── input │ │ │ ├── AsyncEditor.ts │ │ │ ├── AutocompleteDropdown.vue │ │ │ ├── Button.story.vue │ │ │ ├── Button.vue │ │ │ ├── ColorPicker.story.vue │ │ │ ├── ColorPicker.vue │ │ │ ├── Datepicker.vue │ │ │ ├── DatepickerInline.vue │ │ │ ├── FancyCheckbox.story.vue │ │ │ ├── FancyCheckbox.vue │ │ │ ├── Multiselect.vue │ │ │ ├── Password.vue │ │ │ ├── Reactions.vue │ │ │ ├── SimpleButton.vue │ │ │ └── editor │ │ │ │ ├── CommandsList.vue │ │ │ │ ├── EditorToolbar.vue │ │ │ │ ├── TipTap.vue │ │ │ │ ├── commands.ts │ │ │ │ ├── setLinkInEditor.ts │ │ │ │ ├── suggestion.ts │ │ │ │ └── types.ts │ │ ├── misc │ │ │ ├── ApiConfig.vue │ │ │ ├── ButtonLink.vue │ │ │ ├── Card.story.vue │ │ │ ├── Card.vue │ │ │ ├── ColorBubble.vue │ │ │ ├── CreateEdit.vue │ │ │ ├── CustomTransition.vue │ │ │ ├── Done.vue │ │ │ ├── Dropdown.vue │ │ │ ├── DropdownItem.vue │ │ │ ├── Error.vue │ │ │ ├── Icon.ts │ │ │ ├── Legal.vue │ │ │ ├── Loading.vue │ │ │ ├── Message.vue │ │ │ ├── Modal.vue │ │ │ ├── NoAuthWrapper.vue │ │ │ ├── Nothing.vue │ │ │ ├── Notification.vue │ │ │ ├── OpenQuickActions.vue │ │ │ ├── Pagination.vue │ │ │ ├── PaginationEmit.vue │ │ │ ├── Popup.vue │ │ │ ├── ProgressBar.story.vue │ │ │ ├── ProgressBar.vue │ │ │ ├── Ready.vue │ │ │ ├── Shortcut.vue │ │ │ ├── Subscription.vue │ │ │ ├── User.vue │ │ │ ├── flatpickr │ │ │ │ └── Flatpickr.vue │ │ │ └── keyboard-shortcuts │ │ │ │ ├── index.vue │ │ │ │ └── shortcuts.ts │ │ ├── notifications │ │ │ └── Notifications.vue │ │ ├── project │ │ │ ├── ProjectSettingsDropdown.vue │ │ │ ├── ProjectWrapper.vue │ │ │ ├── partials │ │ │ │ ├── FilterInput.story.vue │ │ │ │ ├── FilterInput.vue │ │ │ │ ├── FilterInputDocs.vue │ │ │ │ ├── FilterPopup.vue │ │ │ │ ├── Filters.vue │ │ │ │ ├── ProjectCard.vue │ │ │ │ └── ProjectCardGrid.vue │ │ │ └── views │ │ │ │ ├── ProjectGantt.vue │ │ │ │ ├── ProjectKanban.vue │ │ │ │ ├── ProjectList.vue │ │ │ │ ├── ProjectTable.vue │ │ │ │ └── ViewEditForm.vue │ │ ├── quick-actions │ │ │ └── QuickActions.vue │ │ ├── sharing │ │ │ ├── LinkSharing.vue │ │ │ └── UserTeam.vue │ │ └── tasks │ │ │ ├── AddTask.vue │ │ │ ├── GanttChart.vue │ │ │ ├── TaskForm.vue │ │ │ └── partials │ │ │ ├── AssigneeList.vue │ │ │ ├── Attachments.vue │ │ │ ├── ChecklistSummary.vue │ │ │ ├── Comments.vue │ │ │ ├── CreatedUpdated.vue │ │ │ ├── DateTableCell.vue │ │ │ ├── DeferTask.vue │ │ │ ├── Description.vue │ │ │ ├── EditAssignees.vue │ │ │ ├── EditLabels.vue │ │ │ ├── FilePreview.vue │ │ │ ├── Heading.vue │ │ │ ├── KanbanCard.vue │ │ │ ├── Label.vue │ │ │ ├── Labels.vue │ │ │ ├── PercentDoneSelect.vue │ │ │ ├── PriorityLabel.vue │ │ │ ├── PrioritySelect.vue │ │ │ ├── ProjectSearch.vue │ │ │ ├── QuickAddMagic.vue │ │ │ ├── RelatedTasks.vue │ │ │ ├── ReminderDetail.vue │ │ │ ├── ReminderPeriod.vue │ │ │ ├── Reminders.story.vue │ │ │ ├── Reminders.vue │ │ │ ├── RepeatAfter.vue │ │ │ ├── SingleTaskInProject.vue │ │ │ ├── SingleTaskInlineReadonly.vue │ │ │ └── Sort.vue │ ├── composables │ │ ├── useAutoHeightTextarea.ts │ │ ├── useBodyClass.ts │ │ ├── useColorScheme.ts │ │ ├── useCopyToClipboard.ts │ │ ├── useDaytimeSalutation.ts │ │ ├── useGlobalNow.ts │ │ ├── useMenuActive.ts │ │ ├── useOnline.ts │ │ ├── useProjectBackground.ts │ │ ├── useRedirectToLastVisited.ts │ │ ├── useRenewTokenOnFocus.ts │ │ ├── useRouteFilters.ts │ │ ├── useRouteWithModal.ts │ │ ├── useTaskList.ts │ │ └── useTitle.ts │ ├── constants │ │ ├── date.ts │ │ ├── linkShareHash.ts │ │ ├── priorities.ts │ │ └── rights.ts │ ├── directives │ │ ├── cypress.ts │ │ ├── focus.ts │ │ └── shortcut.ts │ ├── helpers │ │ ├── attachments.ts │ │ ├── auth.ts │ │ ├── calculateItemPosition.ts │ │ ├── calculateTaskPosition.test.ts │ │ ├── case.ts │ │ ├── checkAndSetApiUrl.ts │ │ ├── checklistFromText.test.ts │ │ ├── checklistFromText.ts │ │ ├── closeWhenClickedOutside.ts │ │ ├── color │ │ │ ├── colorFromHex.test.ts │ │ │ ├── colorFromHex.ts │ │ │ ├── colorIsDark.test.ts │ │ │ ├── colorIsDark.ts │ │ │ └── randomColor.ts │ │ ├── createAsyncComponent.ts │ │ ├── downloadBlob.ts │ │ ├── editorContentEmpty.ts │ │ ├── fetcher.ts │ │ ├── filters.test.ts │ │ ├── filters.ts │ │ ├── getBlobFromBlurHash.ts │ │ ├── getFullBaseUrl.ts │ │ ├── getHumanSize.ts │ │ ├── getInheritedBackgroundColor.ts │ │ ├── getProjectTitle.ts │ │ ├── hourToDaytime.test.ts │ │ ├── hourToDaytime.ts │ │ ├── inputPrompt.ts │ │ ├── isAppleDevice.ts │ │ ├── isEmail.ts │ │ ├── isValidHttpUrl.ts │ │ ├── parseDateOrNull.ts │ │ ├── parseSubtasksViaIndention.test.ts │ │ ├── parseSubtasksViaIndention.ts │ │ ├── playPop.ts │ │ ├── projectView.ts │ │ ├── randomId.ts │ │ ├── redirectToProvider.ts │ │ ├── replaceAll.ts │ │ ├── saveCollapsedBucketState.ts │ │ ├── saveLastVisited.ts │ │ ├── scrollIntoView.ts │ │ ├── setTitle.ts │ │ ├── time │ │ │ ├── calculateDayInterval.test.ts │ │ │ ├── calculateDayInterval.ts │ │ │ ├── calculateNearestHours.ts │ │ │ ├── calculateNearestTime.test.ts │ │ │ ├── createDateFromString.test.ts │ │ │ ├── createDateFromString.ts │ │ │ ├── formatDate.ts │ │ │ ├── getNextWeekDate.ts │ │ │ ├── isoToKebabDate.ts │ │ │ ├── parseBooleanProp.ts │ │ │ ├── parseDate.ts │ │ │ ├── parseDateOrString.ts │ │ │ ├── parseDateProp.ts │ │ │ └── period.ts │ │ ├── useFlatpickrLanguage.ts │ │ ├── utils.ts │ │ └── validatePasswort.ts │ ├── histoire.setup.ts │ ├── i18n │ │ ├── index.ts │ │ ├── lang │ │ │ ├── ar-SA.json │ │ │ ├── bg-BG.json │ │ │ ├── ca-ES.json │ │ │ ├── cs-CZ.json │ │ │ ├── da-DK.json │ │ │ ├── de-DE.json │ │ │ ├── de-swiss.json │ │ │ ├── en.json │ │ │ ├── eo-UY.json │ │ │ ├── es-ES.json │ │ │ ├── fi-FI.json │ │ │ ├── fr-FR.json │ │ │ ├── he-IL.json │ │ │ ├── hr-HR.json │ │ │ ├── hu-HU.json │ │ │ ├── it-IT.json │ │ │ ├── ja-JP.json │ │ │ ├── ko-KR.json │ │ │ ├── lt-LT.json │ │ │ ├── nl-NL.json │ │ │ ├── no-NO.json │ │ │ ├── pl-PL.json │ │ │ ├── pt-BR.json │ │ │ ├── pt-PT.json │ │ │ ├── ro-RO.json │ │ │ ├── ru-RU.json │ │ │ ├── sk-SK.json │ │ │ ├── sl-SI.json │ │ │ ├── sr-CS.json │ │ │ ├── sv-SE.json │ │ │ ├── tr-TR.json │ │ │ ├── uk-UA.json │ │ │ ├── vi-VN.json │ │ │ ├── zh-CN.json │ │ │ └── zh-TW.json │ │ └── useDayjsLanguageSync.ts │ ├── indexes │ │ └── index.ts │ ├── main.ts │ ├── message │ │ └── index.ts │ ├── modelSchema │ │ └── common │ │ │ └── repeats.ts │ ├── modelTypes │ │ ├── IAbstract.ts │ │ ├── IApiToken.ts │ │ ├── IAttachment.ts │ │ ├── IAvatar.ts │ │ ├── IBackgroundImage.ts │ │ ├── IBucket.ts │ │ ├── ICaldavToken.ts │ │ ├── IEmailUpdate.ts │ │ ├── IFile.ts │ │ ├── ILabel.ts │ │ ├── ILabelTask.ts │ │ ├── ILinkShare.ts │ │ ├── INotification.ts │ │ ├── IPasswordReset.ts │ │ ├── IPasswordUpdate.ts │ │ ├── IProject.ts │ │ ├── IProjectDuplicate.ts │ │ ├── IProjectView.ts │ │ ├── IReaction.ts │ │ ├── ISavedFilter.ts │ │ ├── ISubscription.ts │ │ ├── ITask.ts │ │ ├── ITaskAssignee.ts │ │ ├── ITaskBucket.ts │ │ ├── ITaskComment.ts │ │ ├── ITaskPosition.ts │ │ ├── ITaskRelation.ts │ │ ├── ITaskReminder.ts │ │ ├── ITeam.ts │ │ ├── ITeamMember.ts │ │ ├── ITeamProject.ts │ │ ├── ITeamShareBase.ts │ │ ├── ITotp.ts │ │ ├── IUser.ts │ │ ├── IUserProject.ts │ │ ├── IUserSettings.ts │ │ ├── IUserShareBase.ts │ │ └── IWebhook.ts │ ├── models │ │ ├── abstractModel.ts │ │ ├── apiTokenModel.ts │ │ ├── attachment.ts │ │ ├── avatar.ts │ │ ├── backgroundImage.ts │ │ ├── bucket.ts │ │ ├── caldavToken.ts │ │ ├── emailUpdate.ts │ │ ├── file.ts │ │ ├── label.ts │ │ ├── labelTask.ts │ │ ├── linkShare.ts │ │ ├── notification.ts │ │ ├── passwordReset.ts │ │ ├── passwordUpdate.ts │ │ ├── project.ts │ │ ├── projectDuplicateModel.ts │ │ ├── projectView.ts │ │ ├── reaction.ts │ │ ├── savedFilter.ts │ │ ├── subscription.ts │ │ ├── task.ts │ │ ├── taskAssignee.ts │ │ ├── taskBucket.ts │ │ ├── taskComment.ts │ │ ├── taskPosition.ts │ │ ├── taskRelation.ts │ │ ├── taskReminder.ts │ │ ├── team.ts │ │ ├── teamMember.ts │ │ ├── teamProject.ts │ │ ├── teamShareBase.ts │ │ ├── totp.ts │ │ ├── user.ts │ │ ├── userProject.ts │ │ ├── userSettings.ts │ │ ├── userShareBase.ts │ │ └── webhook.ts │ ├── modules │ │ ├── parseTaskText.test.ts │ │ ├── parseTaskText.ts │ │ ├── projectHistory.test.ts │ │ └── projectHistory.ts │ ├── pinia.ts │ ├── registerServiceWorker.ts │ ├── router │ │ └── index.ts │ ├── sentry.ts │ ├── services │ │ ├── abstractService.ts │ │ ├── accountDelete.ts │ │ ├── apiToken.ts │ │ ├── attachment.ts │ │ ├── avatar.ts │ │ ├── backgroundUnsplash.ts │ │ ├── backgroundUpload.ts │ │ ├── bucket.ts │ │ ├── caldavToken.ts │ │ ├── dataExport.ts │ │ ├── emailUpdate.ts │ │ ├── label.ts │ │ ├── labelTask.ts │ │ ├── linkShare.ts │ │ ├── migrator │ │ │ ├── abstractMigration.ts │ │ │ └── abstractMigrationFile.ts │ │ ├── notification.ts │ │ ├── passwordReset.ts │ │ ├── passwordUpdateService.ts │ │ ├── project.ts │ │ ├── projectDuplicateService.ts │ │ ├── projectUsers.ts │ │ ├── projectViews.ts │ │ ├── reactions.ts │ │ ├── savedFilter.ts │ │ ├── subscription.ts │ │ ├── task.ts │ │ ├── taskAssignee.ts │ │ ├── taskBucket.ts │ │ ├── taskCollection.ts │ │ ├── taskComment.ts │ │ ├── taskPosition.ts │ │ ├── taskRelation.ts │ │ ├── team.ts │ │ ├── teamMember.ts │ │ ├── teamProject.ts │ │ ├── totp.ts │ │ ├── user.ts │ │ ├── userProject.ts │ │ ├── userSettings.ts │ │ └── webhook.ts │ ├── stores │ │ ├── attachments.ts │ │ ├── auth.ts │ │ ├── base.ts │ │ ├── config.ts │ │ ├── helper.ts │ │ ├── kanban.ts │ │ ├── labels.test.ts │ │ ├── labels.ts │ │ ├── projects.ts │ │ └── tasks.ts │ ├── styles │ │ ├── common-imports.scss │ │ ├── components │ │ │ ├── _index.scss │ │ │ ├── labels.scss │ │ │ ├── task.scss │ │ │ ├── tasks.scss │ │ │ └── tooltip.scss │ │ ├── custom-properties │ │ │ ├── _index.scss │ │ │ ├── colors.scss │ │ │ └── shadows.scss │ │ ├── fonts.scss │ │ ├── global.scss │ │ ├── theme │ │ │ ├── _index.scss │ │ │ ├── background.scss │ │ │ ├── content.scss │ │ │ ├── flatpickr.scss │ │ │ ├── form.scss │ │ │ ├── helpers.scss │ │ │ ├── link-share.scss │ │ │ ├── loading.scss │ │ │ ├── navigation.scss │ │ │ ├── scrollbars.scss │ │ │ └── theme.scss │ │ └── transitions.scss │ ├── sw.ts │ ├── types │ │ ├── DateISO.ts │ │ ├── IProvider.ts │ │ ├── IRelationKind.ts │ │ ├── IReminderPeriodRelativeTo.ts │ │ ├── IRepeatAfter.ts │ │ ├── IRepeatMode.ts │ │ ├── PartialWithId.ts │ │ ├── cypress.d.ts │ │ ├── global-components.d.ts │ │ └── vue-fontawesome.ts │ ├── urls.ts │ ├── version.json │ └── views │ │ ├── 404.vue │ │ ├── About.vue │ │ ├── Home.vue │ │ ├── filters │ │ ├── FilterDelete.vue │ │ ├── FilterEdit.vue │ │ └── FilterNew.vue │ │ ├── labels │ │ ├── ListLabels.vue │ │ └── NewLabel.vue │ │ ├── migrate │ │ ├── Migration.vue │ │ ├── MigrationHandler.vue │ │ ├── icons │ │ │ ├── microsoft-todo.svg │ │ │ ├── ticktick.svg │ │ │ ├── todoist.svg │ │ │ ├── trello.svg │ │ │ ├── vikunja-file.png │ │ │ └── wunderlist.jpg │ │ └── migrators.ts │ │ ├── project │ │ ├── ListProjects.vue │ │ ├── NewProject.vue │ │ ├── ProjectInfo.vue │ │ ├── ProjectView.vue │ │ ├── helpers │ │ │ ├── useGanttFilters.ts │ │ │ └── useGanttTaskList.ts │ │ └── settings │ │ │ ├── ProjectSettingsArchive.vue │ │ │ ├── ProjectSettingsBackground.vue │ │ │ ├── ProjectSettingsDelete.vue │ │ │ ├── ProjectSettingsDuplicate.vue │ │ │ ├── ProjectSettingsEdit.vue │ │ │ ├── ProjectSettingsShare.vue │ │ │ ├── ProjectSettingsViews.vue │ │ │ └── ProjectSettingsWebhooks.vue │ │ ├── sharing │ │ └── LinkSharingAuth.vue │ │ ├── tasks │ │ ├── ShowTasks.vue │ │ └── TaskDetailView.vue │ │ ├── teams │ │ ├── EditTeam.vue │ │ ├── ListTeams.vue │ │ └── NewTeam.vue │ │ └── user │ │ ├── DataExportDownload.vue │ │ ├── Login.vue │ │ ├── OpenIdAuth.vue │ │ ├── PasswordReset.vue │ │ ├── Register.vue │ │ ├── RequestPasswordReset.vue │ │ ├── Settings.vue │ │ └── settings │ │ ├── ApiTokens.vue │ │ ├── Avatar.vue │ │ ├── Caldav.vue │ │ ├── DataExport.vue │ │ ├── Deletion.vue │ │ ├── EmailUpdate.vue │ │ ├── General.vue │ │ ├── PasswordUpdate.vue │ │ └── TOTP.vue ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.config.json ├── tsconfig.json ├── tsconfig.vitest.json └── vite.config.ts ├── go.mod ├── go.sum ├── magefile.go ├── main.go ├── nfpm.yaml ├── pkg ├── caldav │ ├── caldav.go │ ├── caldav_test.go │ ├── parsing.go │ ├── parsing_test.go │ ├── priority.go │ └── priority_test.go ├── cmd │ ├── cmd.go │ ├── dump.go │ ├── healthcheck.go │ ├── index.go │ ├── maintenance.go │ ├── migrate.go │ ├── restore.go │ ├── testmail.go │ ├── user.go │ ├── version.go │ └── web.go ├── config │ └── config.go ├── cron │ └── cron.go ├── db │ ├── db.go │ ├── dump.go │ ├── fixtures │ │ ├── api_tokens.yml │ │ ├── buckets.yml │ │ ├── favorites.yml │ │ ├── files.yml │ │ ├── label_tasks.yml │ │ ├── labels.yml │ │ ├── link_shares.yml │ │ ├── project_views.yml │ │ ├── projects.yml │ │ ├── reactions.yml │ │ ├── saved_filters.yml │ │ ├── subscriptions.yml │ │ ├── task_assignees.yml │ │ ├── task_attachments.yml │ │ ├── task_buckets.yml │ │ ├── task_comments.yml │ │ ├── task_positions.yml │ │ ├── task_relations.yml │ │ ├── task_reminders.yml │ │ ├── tasks.yml │ │ ├── team_members.yml │ │ ├── team_projects.yml │ │ ├── teams.yml │ │ ├── user_tokens.yml │ │ ├── users.yml │ │ └── users_projects.yml │ ├── helpers.go │ ├── test.go │ └── test_fixtures.go ├── events │ ├── events.go │ ├── listeners.go │ └── testing.go ├── files │ ├── db.go │ ├── dump.go │ ├── error.go │ ├── filehandling.go │ ├── files.go │ ├── files_test.go │ └── main_test.go ├── i18n │ ├── i18n.go │ └── lang │ │ ├── ar-SA.json │ │ ├── bg-BG.json │ │ ├── ca-ES.json │ │ ├── cs-CZ.json │ │ ├── da-DK.json │ │ ├── de-DE.json │ │ ├── de-swiss.json │ │ ├── en.json │ │ ├── eo-UY.json │ │ ├── es-ES.json │ │ ├── fi-FI.json │ │ ├── fr-FR.json │ │ ├── he-IL.json │ │ ├── hr-HR.json │ │ ├── hu-HU.json │ │ ├── it-IT.json │ │ ├── ja-JP.json │ │ ├── ko-KR.json │ │ ├── lt-LT.json │ │ ├── nl-NL.json │ │ ├── no-NO.json │ │ ├── pl-PL.json │ │ ├── pt-BR.json │ │ ├── pt-PT.json │ │ ├── ro-RO.json │ │ ├── ru-RU.json │ │ ├── sk-SK.json │ │ ├── sl-SI.json │ │ ├── sr-CS.json │ │ ├── sv-SE.json │ │ ├── tr-TR.json │ │ ├── uk-UA.json │ │ ├── vi-VN.json │ │ ├── zh-CN.json │ │ └── zh-TW.json ├── initialize │ ├── events.go │ └── init.go ├── integrations │ ├── _test.go.tpl │ ├── api_tokens_test.go │ ├── archived_test.go │ ├── caldav_test.go │ ├── healthcheck_test.go │ ├── integrations.go │ ├── kanban_test.go │ ├── link_sharing_auth_test.go │ ├── link_sharing_test.go │ ├── login_test.go │ ├── project_test.go │ ├── register_test.go │ ├── task_collection_test.go │ ├── task_comment_test.go │ ├── task_test.go │ ├── token_test.go │ ├── user_change_password_test.go │ ├── user_confirm_email_test.go │ ├── user_password_request_token_test.go │ ├── user_password_reset_test.go │ ├── user_project_test.go │ └── user_show_test.go ├── log │ ├── logging.go │ ├── mail_logger.go │ ├── noop.go │ ├── watermill_logger.go │ └── xorm_logger.go ├── mail │ ├── mail.go │ ├── send_mail.go │ └── testing.go ├── metrics │ ├── active_users.go │ └── metrics.go ├── migration │ ├── 20190324205606.go │ ├── 20190328074430.go │ ├── 20190430111111.go │ ├── 20190511202210.go │ ├── 20190514192749.go │ ├── 20190524205441.go │ ├── 20190718200716.go │ ├── 20190818210133.go │ ├── 20190920185205.go │ ├── 20190922205826.go │ ├── 20191008194238.go │ ├── 20191010131430.go │ ├── 20191207204427.go │ ├── 20191207220736.go │ ├── 20200120201756.go │ ├── 20200219183248.go │ ├── 20200308205855.go │ ├── 20200308210130.go │ ├── 20200322214440.go │ ├── 20200322214624.go │ ├── 20200417175201.go │ ├── 20200418230432.go │ ├── 20200418230605.go │ ├── 20200420215928.go │ ├── 20200425182634.go │ ├── 20200509103709.go │ ├── 20200515172220.go │ ├── 20200515195546.go │ ├── 20200516123847.go │ ├── 20200524221534.go │ ├── 20200524224611.go │ ├── 20200614113230.go │ ├── 20200621214452.go │ ├── 20200801183357.go │ ├── 20200904101559.go │ ├── 20200905151040.go │ ├── 20200905232458.go │ ├── 20200906184746.go │ ├── 20201025195822.go │ ├── 20201121181647.go │ ├── 20201218152741.go │ ├── 20201218220204.go │ ├── 20201219145028.go │ ├── 20210207192805.go │ ├── 20210209204715.go │ ├── 20210220222121.go │ ├── 20210221111953.go │ ├── 20210321185225.go │ ├── 20210328191017.go │ ├── 20210403145503.go │ ├── 20210403220653.go │ ├── 20210407170753.go │ ├── 20210411113105.go │ ├── 20210411161337.go │ ├── 20210413131057.go │ ├── 20210527105701.go │ ├── 20210603174608.go │ ├── 20210709191101.go │ ├── 20210709211508.go │ ├── 20210711173657.go │ ├── 20210713213622.go │ ├── 20210725153703.go │ ├── 20210727204942.go │ ├── 20210727211037.go │ ├── 20210729142940.go │ ├── 20210802081716.go │ ├── 20210829194722.go │ ├── 20211212151642.go │ ├── 20211212210054.go │ ├── 20220112211537.go │ ├── 20220616145228.go │ ├── 20220815200851.go │ ├── 20221002120521.go │ ├── 20221113170740.go │ ├── 20221228112131.go │ ├── 20230104152903.go │ ├── 20230307171848.go │ ├── 20230611170341.go │ ├── 20230824132533.go │ ├── 20230828125443.go │ ├── 20230831155832.go │ ├── 20230903143017.go │ ├── 20230913202615.go │ ├── 20231022144641.go │ ├── 20231108231513.go │ ├── 20231121191822.go │ ├── 20240114224713.go │ ├── 20240304153738.go │ ├── 20240309111148.go │ ├── 20240311173251.go │ ├── 20240313230538.go │ ├── 20240314214802.go │ ├── 20240315093418.go │ ├── 20240315104205.go │ ├── 20240315110428.go │ ├── 20240329170952.go │ ├── 20240406125227.go │ ├── 20240603172746.go │ ├── 20240919130957.go │ ├── 20241028131622.go │ ├── 20241118123644.go │ ├── 20241119115012.go │ ├── 20250317174522.go │ ├── 20250323212553.go │ └── migration.go ├── models │ ├── api_routes.go │ ├── api_tokens.go │ ├── api_tokens_rights.go │ ├── api_tokens_test.go │ ├── bulk_task.go │ ├── bulk_task_test.go │ ├── error.go │ ├── events.go │ ├── export.go │ ├── favorites.go │ ├── kanban.go │ ├── kanban_rights.go │ ├── kanban_task_bucket.go │ ├── kanban_task_bucket_test.go │ ├── kanban_test.go │ ├── label.go │ ├── label_rights.go │ ├── label_task.go │ ├── label_task_rights.go │ ├── label_task_test.go │ ├── label_test.go │ ├── link_sharing.go │ ├── link_sharing_rights.go │ ├── link_sharing_test.go │ ├── listeners.go │ ├── main_test.go │ ├── mentions.go │ ├── mentions_test.go │ ├── message.go │ ├── models.go │ ├── notifications.go │ ├── notifications_database.go │ ├── project.go │ ├── project_duplicate.go │ ├── project_duplicate_test.go │ ├── project_rights.go │ ├── project_team.go │ ├── project_team_rights.go │ ├── project_team_test.go │ ├── project_test.go │ ├── project_users.go │ ├── project_users_rights.go │ ├── project_users_rights_test.go │ ├── project_users_test.go │ ├── project_view.go │ ├── project_view_rights.go │ ├── reaction.go │ ├── reaction_rights.go │ ├── reaction_test.go │ ├── rights.go │ ├── saved_filters.go │ ├── saved_filters_rights.go │ ├── saved_filters_test.go │ ├── subscription.go │ ├── subscription_rights.go │ ├── subscription_test.go │ ├── task_assignees.go │ ├── task_assignees_rights.go │ ├── task_attachment.go │ ├── task_attachment_rights.go │ ├── task_attachment_test.go │ ├── task_collection.go │ ├── task_collection_filter.go │ ├── task_collection_filter_test.go │ ├── task_collection_sort.go │ ├── task_collection_sort_test.go │ ├── task_collection_test.go │ ├── task_comment_rights.go │ ├── task_comments.go │ ├── task_comments_test.go │ ├── task_overdue_reminder.go │ ├── task_overdue_reminder_test.go │ ├── task_position.go │ ├── task_relation.go │ ├── task_relation_rights.go │ ├── task_relation_test.go │ ├── task_reminder.go │ ├── task_reminder_test.go │ ├── task_search.go │ ├── tasks.go │ ├── tasks_rights.go │ ├── tasks_test.go │ ├── team_members.go │ ├── team_members_rights.go │ ├── team_members_test.go │ ├── team_sync.go │ ├── teams.go │ ├── teams_rights.go │ ├── teams_rights_test.go │ ├── teams_test.go │ ├── typesense.go │ ├── unit_tests.go │ ├── unsplash.go │ ├── user_delete.go │ ├── user_delete_test.go │ ├── user_project.go │ ├── user_project_test.go │ ├── users.go │ ├── webhooks.go │ └── webhooks_rights.go ├── modules │ ├── auth │ │ ├── auth.go │ │ ├── ldap │ │ │ ├── ldap.go │ │ │ ├── ldap_test.go │ │ │ └── main_test.go │ │ └── openid │ │ │ ├── cron.go │ │ │ ├── main_test.go │ │ │ ├── openid.go │ │ │ ├── openid_test.go │ │ │ └── providers.go │ ├── avatar │ │ ├── avatar.go │ │ ├── empty │ │ │ └── empty.go │ │ ├── gravatar │ │ │ └── gravatar.go │ │ ├── initials │ │ │ └── initials.go │ │ ├── ldap │ │ │ └── ldap.go │ │ ├── marble │ │ │ └── marble.go │ │ └── upload │ │ │ └── upload.go │ ├── background │ │ ├── background.go │ │ ├── handler │ │ │ └── background.go │ │ ├── unsplash │ │ │ ├── proxy.go │ │ │ └── unsplash.go │ │ └── upload │ │ │ └── upload.go │ ├── dump │ │ ├── dump.go │ │ └── restore.go │ ├── keyvalue │ │ ├── error │ │ │ └── error.go │ │ ├── keyvalue.go │ │ ├── memory │ │ │ └── memory.go │ │ └── redis │ │ │ └── redis.go │ └── migration │ │ ├── create_from_structure.go │ │ ├── create_from_structure_test.go │ │ ├── db.go │ │ ├── errors.go │ │ ├── handler │ │ ├── common.go │ │ ├── events.go │ │ ├── handler.go │ │ ├── handler_file.go │ │ ├── listeners.go │ │ └── notifications.go │ │ ├── helpers.go │ │ ├── main_test.go │ │ ├── microsoft-todo │ │ ├── microsoft_todo.go │ │ └── microsoft_todo_test.go │ │ ├── migration_status.go │ │ ├── migrator.go │ │ ├── testimage.jpg │ │ ├── ticktick │ │ ├── ticktick.go │ │ └── ticktick_test.go │ │ ├── todoist │ │ ├── todoist.go │ │ └── todoist_test.go │ │ ├── trello │ │ ├── trello.go │ │ └── trello_test.go │ │ └── vikunja-file │ │ ├── export.zip │ │ ├── export_pre_0.21.0.zip │ │ ├── main_test.go │ │ ├── vikunja.go │ │ └── vikunja_test.go ├── notifications │ ├── database.go │ ├── db.go │ ├── logo.png │ ├── mail.go │ ├── mail_render.go │ ├── mail_test.go │ ├── main_test.go │ ├── notification.go │ ├── notification_test.go │ └── testing.go ├── red │ └── redis.go ├── routes │ ├── api │ │ └── v1 │ │ │ ├── avatar.go │ │ │ ├── docs.go │ │ │ ├── info.go │ │ │ ├── link_sharing_auth.go │ │ │ ├── login.go │ │ │ ├── notifications.go │ │ │ ├── task_attachment.go │ │ │ ├── testing.go │ │ │ ├── token_check.go │ │ │ ├── user_caldav_token.go │ │ │ ├── user_confirm_email.go │ │ │ ├── user_deletion.go │ │ │ ├── user_export.go │ │ │ ├── user_list.go │ │ │ ├── user_password_reset.go │ │ │ ├── user_register.go │ │ │ ├── user_settings.go │ │ │ ├── user_show.go │ │ │ ├── user_totp.go │ │ │ ├── user_update_email.go │ │ │ ├── user_update_password.go │ │ │ └── webhooks.go │ ├── api_tokens.go │ ├── caldav │ │ ├── auth.go │ │ ├── handler.go │ │ ├── listStorageProvider.go │ │ └── listStorageProvider_test.go │ ├── healthcheck.go │ ├── metrics.go │ ├── rate_limit.go │ ├── routes.go │ ├── static.go │ └── validation.go ├── swagger │ ├── docs.go │ ├── swagger.json │ └── swagger.yaml ├── user │ ├── caldav_token.go │ ├── db.go │ ├── delete.go │ ├── error.go │ ├── events.go │ ├── listeners.go │ ├── main_test.go │ ├── notifications.go │ ├── test.go │ ├── token.go │ ├── totp.go │ ├── update_email.go │ ├── user.go │ ├── user_create.go │ ├── user_email_confirm.go │ ├── user_email_confirm_test.go │ ├── user_password_reset.go │ ├── user_test.go │ ├── users_project.go │ └── validator.go ├── utils │ ├── duration.go │ ├── duration_test.go │ ├── humanize_duration.go │ ├── humanize_duration_test.go │ ├── md5_string.go │ ├── md5_string_test.go │ ├── normalize_hex.go │ ├── normalize_hex_test.go │ ├── random.go │ ├── random_test.go │ ├── sha256.go │ ├── sha256_test.go │ ├── slice_difference.go │ ├── strings.go │ ├── time.go │ ├── umask_unix.go │ ├── umask_windows.go │ └── write_to_zip.go ├── version │ └── version.go └── web │ ├── handler │ ├── create.go │ ├── delete.go │ ├── helper.go │ ├── read_all.go │ ├── read_one.go │ └── update.go │ ├── readme.md │ └── web.go ├── publiccode.yml ├── renovate.json ├── rest ├── bruno.json ├── environments │ └── local.bru ├── login.bru ├── mark all notifications as read.bru └── user info.bru ├── tools.go ├── tsconfig.json └── vikunja.service /.dockerignore: -------------------------------------------------------------------------------- 1 | files/ 2 | dist/ 3 | logs/ 4 | docs/ 5 | .devenv/ 6 | .direnv/ 7 | .idea/ 8 | 9 | Dockerfile 10 | docker-manifest.tmpl 11 | docker-manifest-unstable.tmpl 12 | *.db 13 | *.zip 14 | 15 | # Frontend 16 | /frontend/node_modules/ 17 | /frontend/.direnv 18 | /frontend/dist 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = tab 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = false 11 | insert_final_newline = true 12 | 13 | [*.go] 14 | indent_style = tab 15 | 16 | [*.{yaml,yml}] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.json] 21 | indent_style = space 22 | indent_size = 4 23 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | source_url "https://raw.githubusercontent.com/cachix/devenv/95f329d49a8a5289d31e0982652f7058a189bfca/direnvrc" "sha256-d+8cBpDfDBj41inrADaJt+bDWhOktwslgoP5YiGJ1v0=" 2 | 3 | use devenv -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: kolaente 2 | open_collective: vikunja 3 | custom: ["https://vikunja.cloud", "https://www.buymeacoffee.com/kolaente"] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Forum 4 | url: https://community.vikunja.io/ 5 | about: Feature Requests, Questions, configuration or deployment problems should be discussed in the forum. 6 | - name: Security-related issues 7 | url: https://vikunja.io/contact/#security 8 | about: For security concerns, please send a mail to security@vikunja.io instead of opening a public issue. 9 | - name: Chat on Matrix 10 | url: https://matrix.to/#/#vikunja:matrix.org 11 | about: Please ask any quick questions here. 12 | - name: Translations 13 | url: https://crowdin.com/project/vikunja 14 | about: Any problems or requests for new languages about translations should be handled in crowdin. 15 | -------------------------------------------------------------------------------- /.github/actions/setup-frontend/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup Frontend 2 | description: Common setup for frontend jobs using pnpm 3 | runs: 4 | using: "composite" 5 | steps: 6 | - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 7 | with: 8 | run_install: false 9 | package_json_file: frontend/package.json 10 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 11 | with: 12 | node-version-file: frontend/.nvmrc 13 | cache: 'pnpm' 14 | cache-dependency-path: frontend/pnpm-lock.yaml 15 | - name: Install dependencies 16 | working-directory: frontend 17 | run: pnpm install --frozen-lockfile --prefer-offline 18 | shell: bash 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | push: 6 | tags: 7 | - v* 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | name: Test 14 | uses: ./.github/workflows/test.yml 15 | secrets: inherit 16 | 17 | release: 18 | name: Release 19 | if: ${{ github.ref_type == 'tag' || github.ref_name == 'main' }} 20 | uses: ./.github/workflows/release.yml 21 | needs: 22 | - test 23 | secrets: inherit 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .idea/*/* 3 | .idea/httpRequests 4 | config.yml 5 | config.yaml 6 | !docs/config.yml 7 | !.github/ISSUE_TEMPLATE/config.yml 8 | !.gitea/ISSUE_TEMPLATE/config.yml 9 | docs/themes/ 10 | *.db 11 | Run 12 | dist/ 13 | cover.* 14 | /vikunja 15 | Test_* 16 | bin/ 17 | secrets 18 | *.deb 19 | debian/ 20 | logs/ 21 | docs/public/ 22 | docs/resources/ 23 | pkg/static/templates_vfsdata.go 24 | files/ 25 | !pkg/files/ 26 | vikunja-dump* 27 | vendor/ 28 | os-packages/ 29 | mage_output_file.go 30 | mage-static 31 | .DS_Store 32 | 33 | # Devenv 34 | .devenv* 35 | devenv.local.nix 36 | 37 | # direnv 38 | .direnv 39 | 40 | # pre-commit 41 | .pre-commit-config.yaml 42 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "codezombiech.gitignore", 4 | "dbaeumer.vscode-eslint", 5 | "editorconfig.editorconfig", 6 | "vue.volar", 7 | "lokalise.i18n-ally", 8 | "mikestead.dotenv", 9 | "Syler.sass-indented", 10 | "vitest.explorer", 11 | "mkhl.direnv", 12 | "golang.Go" 13 | ] 14 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${workspaceRoot}", 13 | "env": {}, 14 | "args": [] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.testEnvVars": { 3 | "VIKUNJA_SERVICE_ROOTPATH": "${workspaceRoot}" 4 | }, 5 | "editor.formatOnSave": false, 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll": "explicit" 8 | }, 9 | "eslint.format.enable": true, 10 | "[javascript]": { 11 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 12 | }, 13 | "[typescript]": { 14 | "editor.defaultFormatter": "vscode.typescript-language-features" 15 | }, 16 | 17 | // https://eslint.vuejs.org/user-guide/#editor-integrations 18 | "eslint.validate": [ 19 | "javascript", 20 | "javascriptreact", 21 | "vue" 22 | ], 23 | 24 | // disable vetur in case it's installed 25 | "vetur.validation.template": false, 26 | 27 | // i18n ally 28 | "i18n-ally.localesPaths": [ 29 | "frontend/src/i18n/lang" 30 | ], 31 | "i18n-ally.sortKeys": true, 32 | "i18n-ally.keepFulfilled": true, 33 | "i18n-ally.keystyle": "nested" 34 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Please check out the guidelines on https://vikunja.io/docs/development/ 4 | -------------------------------------------------------------------------------- /build/after-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | systemctl enable vikunja.service 4 | 5 | # Fix the config to contain proper values 6 | NEW_SECRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) 7 | sed -i "s//$NEW_SECRET/g" /etc/vikunja/config.yml 8 | sed -i "s//\/opt\/vikunja\//g" /etc/vikunja/config.yml 9 | sed -i "s/Path: \"\.\/vikunja.db\"/Path: \"\\/opt\/vikunja\/vikunja.db\"/g" /etc/vikunja/config.yml 10 | -------------------------------------------------------------------------------- /build/reprepro-dist-conf: -------------------------------------------------------------------------------- 1 | Origin: dl.vikunja.io 2 | Label: Vikunja 3 | Codename: buster 4 | Architectures: amd64 5 | Components: main 6 | Description: The debian repo for Vikunja builds. 7 | SignWith: yes 8 | Pull: buster 9 | -------------------------------------------------------------------------------- /code-header-template.txt: -------------------------------------------------------------------------------- 1 | Vikunja is a to-do list application to facilitate your life. 2 | Copyright 2018-present Vikunja and contributors. All rights reserved. 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public Licensee as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public Licensee for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public Licensee 15 | along with this program. If not, see . -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | "project_id": "462614" 2 | "api_token_env": "CROWDIN_PERSONAL_TOKEN" 3 | "base_path": "." 4 | "base_url": "https://api.crowdin.com" 5 | 6 | "preserve_hierarchy": true 7 | 8 | files: [ 9 | { 10 | "source": "pkg/i18n/lang/en.json", 11 | "translation": "pkg/i18n/lang/%locale%.json", 12 | "dest": "en-api.json", 13 | "type": "json", 14 | }, 15 | { 16 | "source": "frontend/src/i18n/lang/en.json", 17 | "translation": "frontend/src/i18n/lang/%locale%.json", 18 | "dest": "en.json", 19 | "type": "json", 20 | }, 21 | ] -------------------------------------------------------------------------------- /desktop/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | frontend/ 4 | dist/ 5 | *.zip 6 | *.tgz 7 | -------------------------------------------------------------------------------- /desktop/build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/desktop/build/icon.icns -------------------------------------------------------------------------------- /desktop/build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/desktop/build/icon.png -------------------------------------------------------------------------------- /desktop/portInUse.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | 3 | module.exports = function(port, callback) { 4 | const server = net.createServer(function(socket) { 5 | socket.write('Echo server\r\n'); 6 | socket.pipe(socket); 7 | }) 8 | 9 | server.listen(port, '127.0.0.1'); 10 | server.on('error', function (e) { 11 | callback(true) 12 | }) 13 | server.on('listening', function (e) { 14 | server.close() 15 | callback(false) 16 | }) 17 | } 18 | 19 | -------------------------------------------------------------------------------- /devenv.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://devenv.sh/devenv.schema.json 2 | inputs: 3 | nixpkgs: 4 | url: github:cachix/devenv-nixpkgs/rolling 5 | nixpkgs-unstable: 6 | url: github:NixOS/nixpkgs/nixos-unstable 7 | 8 | # If you're using non-OSS software, you can set allowUnfree to true. 9 | allowUnfree: true 10 | 11 | # If you're willing to use a package that's vulnerable 12 | # permittedInsecurePackages: 13 | # - "openssl-1.1.1w" 14 | 15 | # If you have more than one devenv you can merge them 16 | #imports: 17 | # - ./backend 18 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = tab 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = false 11 | insert_final_newline = true 12 | 13 | [*.vue] 14 | indent_style = tab 15 | 16 | [*.{yaml,yml}] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.json] 21 | indent_style = space 22 | indent_size = 2 23 | 24 | [*.{scss,css}] 25 | indent_style = space 26 | indent_size = 2 27 | 28 | [.nvmrc] 29 | insert_final_newline = false 30 | -------------------------------------------------------------------------------- /frontend/.env.local.example: -------------------------------------------------------------------------------- 1 | # (1) Duplicate this file and remove the '.example' suffix. 2 | # Naming this file '.env.local' is a Vite convention to prevent accidentally 3 | # submitting to git. 4 | # For more info see: https://vitejs.dev/guide/env-and-mode.html#env-files 5 | 6 | # (2) Comment in and adjust the values as needed. 7 | 8 | # VITE_IS_ONLINE=true 9 | # SENTRY_AUTH_TOKEN=YOUR_TOKEN 10 | # SENTRY_ORG=vikunja 11 | # SENTRY_PROJECT=frontend-oss 12 | # VIKUNJA_FRONTEND_BASE=/custom-subpath 13 | 14 | # DEV_PROXY=http://vikunja-backend.domain.tld -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | stats.html 10 | 11 | node_modules 12 | .DS_Store 13 | dist 14 | dist-dev 15 | dist-test 16 | coverage 17 | *.zip 18 | .vite/ 19 | 20 | # Test files 21 | cypress/screenshots 22 | cypress/videos 23 | 24 | # local env files 25 | .env.local 26 | .env.*.local 27 | 28 | # Editor directories and files 29 | .vscode 30 | .idea 31 | *.suo 32 | *.ntvs* 33 | *.njsproj 34 | *.sln 35 | *.sw* 36 | !rollup.sw.js 37 | 38 | 39 | # Local Netlify folder 40 | .netlify 41 | 42 | # histoire 43 | .histoire -------------------------------------------------------------------------------- /frontend/.npmrc: -------------------------------------------------------------------------------- 1 | # https://github.com/pnpm/pnpm/issues/8378#issuecomment-2636152421 2 | public-hoist-pattern[]=*eslint* 3 | 4 | # Make sure to install Cypress binary 5 | # https://github.com/cypress-io/github-action/blob/108b8684ae52e735ff7891524cbffbcd4be5b19f/README.md#pnpm 6 | side-effects-cache=false 7 | -------------------------------------------------------------------------------- /frontend/.nvmrc: -------------------------------------------------------------------------------- 1 | 22.16.0 -------------------------------------------------------------------------------- /frontend/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'cypress' 2 | 3 | export default defineConfig({ 4 | env: { 5 | API_URL: 'http://localhost:3456/api/v1', 6 | TEST_SECRET: 'averyLongSecretToSe33dtheDB', 7 | }, 8 | video: false, 9 | retries: { 10 | runMode: 2, 11 | }, 12 | projectId: '181c7x', 13 | e2e: { 14 | specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}', 15 | baseUrl: 'http://127.0.0.1:4173', 16 | experimentalRunAllSpecs: true, 17 | // testIsolation: false, 18 | }, 19 | component: { 20 | devServer: { 21 | framework: 'vue', 22 | bundler: 'vite', 23 | }, 24 | }, 25 | viewportWidth: 1600, 26 | viewportHeight: 900, 27 | experimentalMemoryManagement: true, 28 | }) 29 | -------------------------------------------------------------------------------- /frontend/cypress/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | api: 5 | image: vikunja/api:unstable@sha256:61b77af0f0ed5b0e4c3ee693a79926d8633712ddb245f8212ba5e5321485d330 6 | environment: 7 | VIKUNJA_LOG_LEVEL: DEBUG 8 | VIKUNJA_SERVICE_TESTINGTOKEN: averyLongSecretToSe33dtheDB 9 | ports: 10 | - 3456:3456 11 | cypress: 12 | image: cypress/browsers:node18.12.0-chrome107@sha256:bfdbf9b64fdaad364f6e76e3c2a75fbce7c8018644d71e41ef43bba0ae8f4e38 13 | volumes: 14 | - ..:/project 15 | - $HOME/.cache:/home/node/.cache/ 16 | user: node 17 | working_dir: /project 18 | environment: 19 | CYPRESS_API_URL: http://api:3456/api/v1 20 | CYPRESS_TEST_SECRET: averyLongSecretToSe33dtheDB 21 | -------------------------------------------------------------------------------- /frontend/cypress/e2e/misc/menu.spec.ts: -------------------------------------------------------------------------------- 1 | import {createFakeUserAndLogin} from '../../support/authenticateUser' 2 | 3 | describe('The Menu', () => { 4 | createFakeUserAndLogin() 5 | 6 | beforeEach(() => { 7 | cy.visit('/') 8 | }) 9 | 10 | it('Is visible by default on desktop', () => { 11 | cy.get('.menu-container') 12 | .should('have.class', 'is-active') 13 | }) 14 | 15 | it('Can be hidden on desktop', () => { 16 | cy.get('button.menu-show-button:visible') 17 | .click() 18 | cy.get('.menu-container') 19 | .should('not.have.class', 'is-active') 20 | }) 21 | 22 | it('Is hidden by default on mobile', () => { 23 | cy.viewport('iphone-8') 24 | cy.get('.menu-container') 25 | .should('not.have.class', 'is-active') 26 | }) 27 | 28 | it('Is can be shown on mobile', () => { 29 | cy.viewport('iphone-8') 30 | cy.get('button.menu-show-button:visible') 31 | .click() 32 | cy.get('.menu-container') 33 | .should('have.class', 'is-active') 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /frontend/cypress/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["./**/*", "../support/**/*", "../factories/**/*"], 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "isolatedModules": false, 7 | "target": "ES2015", 8 | "lib": ["ESNext", "dom"], 9 | "types": ["cypress"], 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/cypress/factories/bucket.ts: -------------------------------------------------------------------------------- 1 | import {faker} from '@faker-js/faker' 2 | import {Factory} from '../support/factory' 3 | 4 | export class BucketFactory extends Factory { 5 | static table = 'buckets' 6 | 7 | static factory() { 8 | const now = new Date() 9 | 10 | return { 11 | id: '{increment}', 12 | title: faker.lorem.words(3), 13 | project_view_id: '{increment}', 14 | created_by_id: 1, 15 | created: now.toISOString(), 16 | updated: now.toISOString(), 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/cypress/factories/label_task.ts: -------------------------------------------------------------------------------- 1 | import {Factory} from '../support/factory' 2 | 3 | export class LabelTaskFactory extends Factory { 4 | static table = 'label_tasks' 5 | 6 | static factory() { 7 | const now = new Date() 8 | 9 | return { 10 | id: '{increment}', 11 | task_id: 1, 12 | label_id: 1, 13 | created: now.toISOString(), 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /frontend/cypress/factories/labels.ts: -------------------------------------------------------------------------------- 1 | import {faker} from '@faker-js/faker' 2 | 3 | import {Factory} from '../support/factory' 4 | 5 | export class LabelFactory extends Factory { 6 | static table = 'labels' 7 | 8 | static factory() { 9 | const now = new Date() 10 | 11 | return { 12 | id: '{increment}', 13 | title: faker.lorem.words(2), 14 | description: faker.lorem.text(10), 15 | hex_color: (Math.random()*0xFFFFFF<<0).toString(16), // random 6-digit hex number 16 | created_by_id: 1, 17 | created: now.toISOString(), 18 | updated: now.toISOString(), 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /frontend/cypress/factories/link_sharing.ts: -------------------------------------------------------------------------------- 1 | import {Factory} from '../support/factory' 2 | import {faker} from '@faker-js/faker' 3 | 4 | export class LinkShareFactory extends Factory { 5 | static table = 'link_shares' 6 | 7 | static factory() { 8 | const now = new Date() 9 | 10 | return { 11 | id: '{increment}', 12 | hash: faker.lorem.word(32), 13 | project_id: 1, 14 | right: 0, 15 | sharing_type: 0, 16 | shared_by_id: 1, 17 | created: now.toISOString(), 18 | updated: now.toISOString(), 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/cypress/factories/project.ts: -------------------------------------------------------------------------------- 1 | import {Factory} from '../support/factory' 2 | import {faker} from '@faker-js/faker' 3 | 4 | export interface ProjectAttributes { 5 | id: number | string; // Allow string for '{increment}' 6 | title: string; 7 | owner_id: number; 8 | created: string; 9 | updated: string; 10 | } 11 | 12 | export class ProjectFactory extends Factory { 13 | static table = 'projects' 14 | 15 | static factory(): Omit & { id: string } { // id is '{increment}' before seeding 16 | const now = new Date() 17 | 18 | return { 19 | id: '{increment}', 20 | title: faker.lorem.words(3), 21 | owner_id: 1, 22 | created: now.toISOString(), 23 | updated: now.toISOString(), 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /frontend/cypress/factories/project_view.ts: -------------------------------------------------------------------------------- 1 | import {Factory} from '../support/factory' 2 | import {faker} from '@faker-js/faker' 3 | 4 | export class ProjectViewFactory extends Factory { 5 | static table = 'project_views' 6 | 7 | static factory() { 8 | const now = new Date() 9 | 10 | return { 11 | id: '{increment}', 12 | title: faker.lorem.words(3), 13 | project_id: '{increment}', 14 | view_kind: 0, 15 | created: now.toISOString(), 16 | updated: now.toISOString(), 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /frontend/cypress/factories/task.ts: -------------------------------------------------------------------------------- 1 | import {faker} from '@faker-js/faker' 2 | import {Factory} from '../support/factory' 3 | 4 | export class TaskFactory extends Factory { 5 | static table = 'tasks' 6 | 7 | static factory() { 8 | const now = new Date() 9 | 10 | return { 11 | id: '{increment}', 12 | title: faker.lorem.words(3), 13 | done: false, 14 | project_id: 1, 15 | created_by_id: 1, 16 | index: '{increment}', 17 | created: now.toISOString(), 18 | updated: now.toISOString() 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/cypress/factories/task_assignee.ts: -------------------------------------------------------------------------------- 1 | import {Factory} from '../support/factory' 2 | 3 | export class TaskAssigneeFactory extends Factory { 4 | static table = 'task_assignees' 5 | 6 | static factory() { 7 | const now = new Date() 8 | 9 | return { 10 | id: '{increment}', 11 | task_id: 1, 12 | user_id: 1, 13 | created: now.toISOString(), 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /frontend/cypress/factories/task_attachments.ts: -------------------------------------------------------------------------------- 1 | import {Factory} from '../support/factory' 2 | 3 | export class TaskAttachmentFactory extends Factory { 4 | static table = 'task_attachments' 5 | 6 | static factory() { 7 | const now = new Date() 8 | 9 | return { 10 | id: '{increment}', 11 | task_id: 1, 12 | file_id: 1, 13 | created: now.toISOString(), 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /frontend/cypress/factories/task_buckets.ts: -------------------------------------------------------------------------------- 1 | import {Factory} from '../support/factory' 2 | 3 | export class TaskBucketFactory extends Factory { 4 | static table = 'task_buckets' 5 | 6 | static factory() { 7 | return { 8 | task_id: '{increment}', 9 | bucket_id: '{increment}', 10 | project_view_id: '{increment}', 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /frontend/cypress/factories/task_comment.ts: -------------------------------------------------------------------------------- 1 | import {faker} from '@faker-js/faker' 2 | 3 | import {Factory} from '../support/factory' 4 | 5 | export class TaskCommentFactory extends Factory { 6 | static table = 'task_comments' 7 | 8 | static factory() { 9 | const now = new Date() 10 | 11 | return { 12 | id: '{increment}', 13 | comment: faker.lorem.text(3), 14 | author_id: 1, 15 | task_id: 1, 16 | created: now.toISOString(), 17 | updated: now.toISOString() 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/cypress/factories/task_reminders.ts: -------------------------------------------------------------------------------- 1 | import {Factory} from '../support/factory' 2 | 3 | export class TaskReminderFactory extends Factory { 4 | static table = 'task_reminders' 5 | 6 | static factory() { 7 | const now = new Date() 8 | 9 | return { 10 | id: '{increment}', 11 | task_id: 1, 12 | reminder: now.toISOString(), 13 | created: now.toISOString(), 14 | relative_to: '', 15 | relative_period: 0, 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /frontend/cypress/factories/team.ts: -------------------------------------------------------------------------------- 1 | import {faker} from '@faker-js/faker' 2 | import {Factory} from '../support/factory' 3 | 4 | export class TeamFactory extends Factory { 5 | static table = 'teams' 6 | 7 | static factory() { 8 | const now = new Date() 9 | 10 | return { 11 | name: faker.lorem.words(3), 12 | created_by_id: 1, 13 | created: now.toISOString(), 14 | updated: now.toISOString(), 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/cypress/factories/team_member.ts: -------------------------------------------------------------------------------- 1 | import {Factory} from '../support/factory' 2 | 3 | export class TeamMemberFactory extends Factory { 4 | static table = 'team_members' 5 | 6 | static factory() { 7 | return { 8 | team_id: 1, 9 | user_id: 1, 10 | admin: false, 11 | created: new Date().toISOString(), 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /frontend/cypress/factories/token.ts: -------------------------------------------------------------------------------- 1 | import {faker} from '@faker-js/faker' 2 | import {Factory} from '../support/factory' 3 | 4 | export interface TokenAttributes { 5 | id: number | string; // Allow string for '{increment}' 6 | user_id: number; 7 | token: string; 8 | kind: number; 9 | created: string; 10 | } 11 | 12 | export class TokenFactory extends Factory { 13 | static table = 'user_tokens' 14 | 15 | // The factory method itself produces an object where id is '{increment}' (a string) 16 | // before it gets processed by the main create() method in the base Factory class. 17 | static factory(attrs?: Partial>): Omit & { id: string } { 18 | const now = new Date() 19 | 20 | return { 21 | id: '{increment}', // This is a string 22 | user_id: 1, // Default user_id 23 | token: faker.string.alphanumeric(64), 24 | kind: 1, // TokenPasswordReset 25 | created: now.toISOString(), 26 | ...(attrs ?? {}), 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /frontend/cypress/factories/user.ts: -------------------------------------------------------------------------------- 1 | import {faker} from '@faker-js/faker' 2 | 3 | import {Factory} from '../support/factory' 4 | 5 | export interface UserAttributes { 6 | id: number | string; 7 | username: string; 8 | password?: string; 9 | status: number; 10 | issuer: string; 11 | language: string; 12 | created: string; 13 | updated: string; 14 | } 15 | 16 | export class UserFactory extends Factory { 17 | static table = 'users' 18 | 19 | static factory(): Omit & { id: string; password?: string } { 20 | const now = new Date() 21 | 22 | return { 23 | id: '{increment}', 24 | username: faker.lorem.word(10) + faker.string.uuid(), 25 | password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.', // 1234 26 | status: 0, 27 | issuer: 'local', 28 | language: 'en', 29 | created: now.toISOString(), 30 | updated: now.toISOString(), 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /frontend/cypress/factories/users_project.ts: -------------------------------------------------------------------------------- 1 | import {Factory} from '../support/factory' 2 | 3 | export class UserProjectFactory extends Factory { 4 | static table = 'users_projects' 5 | 6 | static factory() { 7 | const now = new Date() 8 | 9 | return { 10 | id: '{increment}', 11 | project_id: 1, 12 | user_id: 1, 13 | right: 0, 14 | created: now.toISOString(), 15 | updated: now.toISOString(), 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /frontend/cypress/fixtures/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/cypress/fixtures/image.jpg -------------------------------------------------------------------------------- /frontend/cypress/support/component.index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /frontend/cypress/support/component.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/component.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | import { mount } from 'cypress/vue' 23 | // Ensure global styles are loaded 24 | import '../../src/styles/global.scss'; 25 | 26 | Cypress.Commands.add('mount', mount) 27 | 28 | // Example use: 29 | // cy.mount(MyComponent) -------------------------------------------------------------------------------- /frontend/cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | 2 | import './commands' 3 | import '@4tw/cypress-drag-drop' 4 | 5 | // see https://github.com/cypress-io/cypress/issues/702#issuecomment-587127275 6 | Cypress.on('window:before:load', (win) => { 7 | // disable service workers 8 | // @ts-ignore 9 | delete win.navigator.__proto__.ServiceWorker 10 | }) -------------------------------------------------------------------------------- /frontend/cypress/support/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace Cypress { 4 | interface Chainable { 5 | /** 6 | * Pastes a file onto the subject element. 7 | * @param fileName The name of the file to paste 8 | * @param fileType The MIME type of the file (defaults to 'image/png') 9 | */ 10 | pasteFile(fileName: string, fileType?: string): Chainable; 11 | } 12 | } -------------------------------------------------------------------------------- /frontend/cypress/support/seed.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Seeds a db table with data. If a data object is provided as the second argument, it will load the fixtures 3 | * file for the table and merge the data from it with the passed data. This allows you to override specific 4 | * fields of the fixtures without having to redeclare the whole fixture. 5 | * 6 | * Passing null as the second argument empties the table. 7 | * 8 | * @param table 9 | * @param data 10 | */ 11 | export function seed(table, data = {}, truncate = true) { 12 | if (data === null) { 13 | data = [] 14 | } 15 | 16 | cy.request({ 17 | method: 'PATCH', 18 | url: `${Cypress.env('API_URL')}/test/${table}?truncate=${truncate ? 'true' : 'false'}`, 19 | headers: { 20 | 'Authorization': Cypress.env('TEST_SECRET'), 21 | }, 22 | body: data, 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /frontend/cypress/support/updateUserSettings.ts: -------------------------------------------------------------------------------- 1 | 2 | export function updateUserSettings(settings) { 3 | const token = `Bearer ${window.localStorage.getItem('token')}` 4 | 5 | return cy.request({ 6 | method: 'GET', 7 | url: `${Cypress.env('API_URL')}/user`, 8 | headers: { 9 | 'Authorization': token, 10 | }, 11 | }) 12 | .its('body') 13 | .then(oldSettings => { 14 | return cy.request({ 15 | method: 'POST', 16 | url: `${Cypress.env('API_URL')}/user/settings/general`, 17 | headers: { 18 | 'Authorization': token, 19 | }, 20 | body: { 21 | ...oldSettings, 22 | ...settings, 23 | }, 24 | }) 25 | }) 26 | } -------------------------------------------------------------------------------- /frontend/embed.go: -------------------------------------------------------------------------------- 1 | // Vikunja is a to-do list application to facilitate your life. 2 | // Copyright 2018-present Vikunja and contributors. All rights reserved. 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public Licensee as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public Licensee for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public Licensee 15 | // along with this program. If not, see . 16 | 17 | package frontend 18 | 19 | import "embed" 20 | 21 | //go:embed dist 22 | var Files embed.FS 23 | -------------------------------------------------------------------------------- /frontend/env.config.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'postcss-easing-gradients'; 2 | -------------------------------------------------------------------------------- /frontend/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | 6 | interface ImportMetaEnv { 7 | readonly VIKUNJA_API_URL?: string 8 | readonly VIKUNJA_HTTP_PORT?: number 9 | readonly VIKUNJA_HTTPS_PORT?: number 10 | 11 | readonly VIKUNJA_SENTRY_ENABLED?: boolean 12 | readonly VIKUNJA_SENTRY_DSN?: string 13 | 14 | readonly SENTRY_AUTH_TOKEN?: string 15 | readonly SENTRY_ORG?: string 16 | readonly SENTRY_PROJECT?: string 17 | 18 | readonly VITE_IS_ONLINE: boolean 19 | 20 | readonly VUE_DEVTOOLS_LAUNCH_EDITOR: VitePluginVueDevToolsOptions.launchEditor 21 | } 22 | 23 | interface ImportMeta { 24 | readonly env: ImportMetaEnv 25 | } -------------------------------------------------------------------------------- /frontend/histoire.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig, defaultColors} from 'histoire' 2 | import {HstVue} from '@histoire/plugin-vue' 3 | import {HstScreenshot} from '@histoire/plugin-screenshot' 4 | 5 | export default defineConfig({ 6 | setupFile: './src/histoire.setup.ts', 7 | storyIgnored: [ 8 | '**/node_modules/**', 9 | '**/dist/**', 10 | // see https://kolaente.dev/vikunja/frontend/pulls/2724#issuecomment-42012 11 | '**/.direnv/**', 12 | ], 13 | plugins: [ 14 | HstVue(), 15 | HstScreenshot({ 16 | // Options here 17 | }), 18 | ], 19 | theme: { 20 | title: 'Vikunja', 21 | colors: { 22 | // https://histoire.dev/guide/config.html#builtin-colors 23 | gray: defaultColors.zinc, 24 | primary: defaultColors.cyan, 25 | }, 26 | // logo: { 27 | // square: './img/square.png', 28 | // light: './img/light.png', 29 | // dark: './img/dark.png', 30 | // }, 31 | logoHref: 'https://vikunja.io', 32 | // favicon: './favicon.ico', 33 | }, 34 | }) -------------------------------------------------------------------------------- /frontend/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "pnpm run build" 3 | publish = "dist-preview" 4 | 5 | [[redirects]] 6 | from = "/api/*" 7 | to = "https://try.vikunja.io/api/:splat" 8 | status = 200 9 | force = true 10 | 11 | [[redirects]] 12 | from = "/*" 13 | to = "/index.html" 14 | status = 200 15 | 16 | [[headers]] 17 | for = "/*" 18 | [headers.values] 19 | X-Frame-Options = "DENY" 20 | X-XSS-Protection = "1; mode=block" 21 | X-Robots-Tag = "noindex" 22 | -------------------------------------------------------------------------------- /frontend/originalMedia/audio/pop.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/audio/pop.mp3 -------------------------------------------------------------------------------- /frontend/originalMedia/audio/pop.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/audio/pop.wav -------------------------------------------------------------------------------- /frontend/originalMedia/fonts/OpenSans-Italic[wdth,wght].ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/fonts/OpenSans-Italic[wdth,wght].ttf -------------------------------------------------------------------------------- /frontend/originalMedia/fonts/OpenSans[wdth,wght].ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/fonts/OpenSans[wdth,wght].ttf -------------------------------------------------------------------------------- /frontend/originalMedia/fonts/Quicksand[wght].ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/fonts/Quicksand[wght].ttf -------------------------------------------------------------------------------- /frontend/originalMedia/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/originalMedia/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /frontend/originalMedia/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /frontend/originalMedia/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /frontend/originalMedia/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /frontend/originalMedia/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /frontend/originalMedia/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /frontend/originalMedia/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/originalMedia/icons/badge-monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/icons/badge-monochrome.png -------------------------------------------------------------------------------- /frontend/originalMedia/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/icons/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/originalMedia/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/icons/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/originalMedia/icons/icon-maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/icons/icon-maskable.png -------------------------------------------------------------------------------- /frontend/originalMedia/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /frontend/originalMedia/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/icons/mstile-150x150.png -------------------------------------------------------------------------------- /frontend/originalMedia/images/llama-nightscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/images/llama-nightscape.png -------------------------------------------------------------------------------- /frontend/originalMedia/images/migration/trello.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/originalMedia/images/migration/wunderlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/originalMedia/images/migration/wunderlist.png -------------------------------------------------------------------------------- /frontend/patches/flexsearch@0.7.43.patch: -------------------------------------------------------------------------------- 1 | diff --git a/package.json b/package.json 2 | index c154e54029c94be444916fb2249941e7182d80ed..54a65c42a42c4627506e016132becc43b47a517c 100644 3 | --- a/package.json 4 | +++ b/package.json 5 | @@ -28,13 +28,11 @@ 6 | "email": "info@nextapps.de" 7 | }, 8 | "main": "dist/flexsearch.bundle.min.js", 9 | - "module": "dist/flexsearch.bundle.module.min.js", 10 | "browser": { 11 | "dist/flexsearch.bundle.min.js": "./dist/flexsearch.bundle.min.js", 12 | "dist/flexsearch.bundle.module.min.js": "./dist/flexsearch.bundle.module.min.js", 13 | "worker_threads": false 14 | }, 15 | - "types": "./index.d.ts", 16 | "scripts": { 17 | "build": "npm run copy && npm run build:bundle", 18 | "build:bundle": "node task/build RELEASE=bundle DEBUG=false SUPPORT_WORKER=true SUPPORT_ENCODER=true SUPPORT_CACHE=true SUPPORT_ASYNC=true SUPPORT_STORE=true SUPPORT_TAGS=true SUPPORT_SUGGESTION=true SUPPORT_SERIALIZE=true SUPPORT_DOCUMENT=true POLYFILL=false", 19 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/images/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/public/images/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/public/images/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/public/images/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /frontend/public/images/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/public/images/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /frontend/public/images/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/public/images/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /frontend/public/images/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/public/images/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /frontend/public/images/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/public/images/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /frontend/public/images/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/public/images/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /frontend/public/images/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/public/images/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/public/images/icons/badge-monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/public/images/icons/badge-monochrome.png -------------------------------------------------------------------------------- /frontend/public/images/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/public/images/icons/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/public/images/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/public/images/icons/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/public/images/icons/icon-maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/public/images/icons/icon-maskable.png -------------------------------------------------------------------------------- /frontend/public/images/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/public/images/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /frontend/public/images/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/public/images/icons/mstile-150x150.png -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /frontend/scripts/deploy-preview-netlify.mjs.sha384: -------------------------------------------------------------------------------- 1 | 2ba5ae4c831fd749296d92f92c5f89339030e22b80be62b1253dc26982e8fd0082e354f884a3ba15293e0b96317ec758 ./scripts/deploy-preview-netlify.mjs 2 | -------------------------------------------------------------------------------- /frontend/src/assets/audio/pop.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/src/assets/audio/pop.mp3 -------------------------------------------------------------------------------- /frontend/src/assets/checkbox.svg: -------------------------------------------------------------------------------- 1 | 2 | Checkbox 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/OpenSans-BoldItalic_3ff98862.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/src/assets/fonts/OpenSans-BoldItalic_3ff98862.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/OpenSans-Bold_eb52363b.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/src/assets/fonts/OpenSans-Bold_eb52363b.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/OpenSans-Italic[wght]_c9a8fe68.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/src/assets/fonts/OpenSans-Italic[wght]_c9a8fe68.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/OpenSans-RegularItalic_48244a7a.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/src/assets/fonts/OpenSans-RegularItalic_48244a7a.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/OpenSans-Regular_d0acb717.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/src/assets/fonts/OpenSans-Regular_d0acb717.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/OpenSans[wght]_54a65da5.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/src/assets/fonts/OpenSans[wght]_54a65da5.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/Quicksand-Bold_20b26f76.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/src/assets/fonts/Quicksand-Bold_20b26f76.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/Quicksand-Regular_3e913e7e.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/src/assets/fonts/Quicksand-Regular_3e913e7e.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/Quicksand-SemiBold_be48a442.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/src/assets/fonts/Quicksand-SemiBold_be48a442.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/Quicksand[wght]_87bdcc7f.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/src/assets/fonts/Quicksand[wght]_87bdcc7f.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/llama-nightscape.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/src/assets/llama-nightscape.jpg -------------------------------------------------------------------------------- /frontend/src/assets/no-auth-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/src/assets/no-auth-image.jpg -------------------------------------------------------------------------------- /frontend/src/components/date/DatemathHelp.story.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /frontend/src/components/home/Logo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 31 | 32 | 39 | -------------------------------------------------------------------------------- /frontend/src/components/home/PoweredByLink.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | 23 | 33 | -------------------------------------------------------------------------------- /frontend/src/components/input/AsyncEditor.ts: -------------------------------------------------------------------------------- 1 | import {createAsyncComponent} from '@/helpers/createAsyncComponent' 2 | 3 | const TipTap = createAsyncComponent(() => import('@/components/input/editor/TipTap.vue')) 4 | 5 | export default TipTap 6 | -------------------------------------------------------------------------------- /frontend/src/components/input/Button.story.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 36 | -------------------------------------------------------------------------------- /frontend/src/components/input/ColorPicker.story.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /frontend/src/components/input/SimpleButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 27 | -------------------------------------------------------------------------------- /frontend/src/components/input/editor/commands.ts: -------------------------------------------------------------------------------- 1 | import {Extension} from '@tiptap/core' 2 | import Suggestion from '@tiptap/suggestion' 3 | 4 | // Copied and adjusted from https://github.com/ueberdosis/tiptap/tree/252acb32d27a0f9af14813eeed83d8a50059a43a/demos/src/Experiments/Commands/Vue 5 | 6 | export default Extension.create({ 7 | name: 'slash-menu-commands', 8 | 9 | addOptions() { 10 | return { 11 | suggestion: { 12 | char: '/', 13 | command: ({editor, range, props}) => { 14 | props.command({editor, range}) 15 | }, 16 | }, 17 | } 18 | }, 19 | 20 | addProseMirrorPlugins() { 21 | return [ 22 | Suggestion({ 23 | editor: this.editor, 24 | ...this.options.suggestion, 25 | }), 26 | ] 27 | }, 28 | }) 29 | -------------------------------------------------------------------------------- /frontend/src/components/input/editor/setLinkInEditor.ts: -------------------------------------------------------------------------------- 1 | import inputPrompt from '@/helpers/inputPrompt' 2 | 3 | export async function setLinkInEditor(pos, editor) { 4 | const previousUrl = editor?.getAttributes('link').href || '' 5 | const url = await inputPrompt(pos, previousUrl) 6 | 7 | // empty 8 | if (url === '') { 9 | editor 10 | ?.chain() 11 | .focus() 12 | .extendMarkRange('link') 13 | .unsetLink() 14 | .run() 15 | 16 | return 17 | } 18 | 19 | // update link 20 | editor 21 | ?.chain() 22 | .focus() 23 | .extendMarkRange('link') 24 | .setLink({href: url, target: '_blank'}) 25 | .run() 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/components/input/editor/types.ts: -------------------------------------------------------------------------------- 1 | export type UploadCallback = (files: File[] | FileList) => Promise 2 | 3 | export interface BottomAction { 4 | title: string 5 | action: () => void, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/components/misc/ButtonLink.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 20 | -------------------------------------------------------------------------------- /frontend/src/components/misc/Card.story.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /frontend/src/components/misc/ColorBubble.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 16 | 25 | -------------------------------------------------------------------------------- /frontend/src/components/misc/Done.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 20 | 21 | 22 | 37 | -------------------------------------------------------------------------------- /frontend/src/components/misc/Error.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 29 | -------------------------------------------------------------------------------- /frontend/src/components/misc/Legal.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 29 | 30 | 38 | -------------------------------------------------------------------------------- /frontend/src/components/misc/Loading.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 16 | 43 | -------------------------------------------------------------------------------- /frontend/src/components/misc/Nothing.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /frontend/src/components/misc/ProgressBar.story.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /frontend/src/components/misc/Shortcut.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 26 | 27 | 45 | -------------------------------------------------------------------------------- /frontend/src/components/project/partials/FilterInput.story.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /frontend/src/components/tasks/partials/DateTableCell.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /frontend/src/components/tasks/partials/Label.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /frontend/src/components/tasks/partials/Labels.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | 27 | 37 | -------------------------------------------------------------------------------- /frontend/src/components/tasks/partials/PercentDoneSelect.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 29 | -------------------------------------------------------------------------------- /frontend/src/components/tasks/partials/Reminders.story.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 27 | -------------------------------------------------------------------------------- /frontend/src/components/tasks/partials/Sort.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 31 | 32 | 37 | -------------------------------------------------------------------------------- /frontend/src/composables/useBodyClass.ts: -------------------------------------------------------------------------------- 1 | import {ref, watchEffect} from 'vue' 2 | import {tryOnBeforeUnmount} from '@vueuse/core' 3 | 4 | export function useBodyClass(className: string, defaultValue = false) { 5 | const isActive = ref(defaultValue) 6 | 7 | watchEffect(() => { 8 | if(isActive.value) { 9 | document.body.classList.add(className) 10 | return 11 | } 12 | 13 | document.body.classList.remove(className) 14 | }) 15 | 16 | tryOnBeforeUnmount(() => isActive.value && document.body.classList.remove(className)) 17 | 18 | return isActive 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/composables/useGlobalNow.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { createGlobalState, useIntervalFn } from '@vueuse/core' 3 | import { onBeforeRouteUpdate } from 'vue-router' 4 | 5 | import { MILLISECONDS_A_SECOND } from '@/constants/date' 6 | 7 | const GLOBAL_NOW_INTERVAL = 60 * MILLISECONDS_A_SECOND 8 | 9 | /** 10 | * A global shared state that provides the current time, updated at a regular interval. 11 | * 12 | * Sharing this state globally ensures that all components accessing this hook use the same time reference, avoiding redundant intervals and ensuring consistency across the application. 13 | */ 14 | export const useGlobalNow = createGlobalState(() => { 15 | const now = ref(new Date()) 16 | 17 | const update = () => now.value = new Date() 18 | 19 | useIntervalFn(update, GLOBAL_NOW_INTERVAL, { immediate: true }) 20 | 21 | // ensure the now value is refreshed when the route changes 22 | onBeforeRouteUpdate(() => { 23 | update() 24 | }) 25 | 26 | return { 27 | now, 28 | update, 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /frontend/src/composables/useOnline.ts: -------------------------------------------------------------------------------- 1 | import {ref} from 'vue' 2 | import {useOnline as useNetworkOnline} from '@vueuse/core' 3 | import type {ConfigurableWindow} from '@vueuse/core' 4 | 5 | export function useOnline(options?: ConfigurableWindow) { 6 | const isOnline = useNetworkOnline(options) 7 | const fakeOnlineState = Boolean(import.meta.env.VITE_IS_ONLINE) 8 | if (isOnline.value === false && fakeOnlineState) { 9 | console.log('Setting fake online state', fakeOnlineState) 10 | return ref(true) 11 | } 12 | return isOnline 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/composables/useRedirectToLastVisited.ts: -------------------------------------------------------------------------------- 1 | import {useRouter} from 'vue-router' 2 | import {getLastVisited, clearLastVisited} from '@/helpers/saveLastVisited' 3 | 4 | export function useRedirectToLastVisited() { 5 | 6 | const router = useRouter() 7 | 8 | function getLastVisitedRoute() { 9 | const last = getLastVisited() 10 | if (last === null) { 11 | return null 12 | } 13 | 14 | clearLastVisited() 15 | return { 16 | name: last.name, 17 | params: last.params, 18 | query: last.query, 19 | } 20 | } 21 | 22 | function redirectIfSaved() { 23 | const lastRoute = getLastVisitedRoute() 24 | if (!lastRoute) { 25 | return router.push({name: 'home'}) 26 | } 27 | 28 | return router.push(lastRoute) 29 | } 30 | 31 | return { 32 | redirectIfSaved, 33 | getLastVisitedRoute, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/composables/useTitle.ts: -------------------------------------------------------------------------------- 1 | import {computed, toValue} from 'vue' 2 | 3 | import {useTitle as useTitleVueUse, type UseTitleOptions, type ReadonlyRefOrGetter, type MaybeRef, type MaybeRefOrGetter} from '@vueuse/core' 4 | 5 | export function useTitle( 6 | newTitle: 7 | | ReadonlyRefOrGetter 8 | | MaybeRef 9 | | MaybeRefOrGetter = null, 10 | options?: UseTitleOptions, 11 | ) { 12 | const pageTitle = computed(() => toValue(newTitle)) 13 | 14 | const completeTitle = computed(() => 15 | (typeof pageTitle.value === 'undefined' || pageTitle.value === '') 16 | ? 'Vikunja' 17 | : `${pageTitle.value} | Vikunja`, 18 | ) 19 | 20 | return useTitleVueUse(completeTitle, options) 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/constants/date.ts: -------------------------------------------------------------------------------- 1 | export const SECONDS_A_MINUTE = 60 2 | export const SECONDS_A_HOUR = SECONDS_A_MINUTE * 60 3 | export const SECONDS_A_DAY = SECONDS_A_HOUR * 24 4 | export const SECONDS_A_WEEK = SECONDS_A_DAY * 7 5 | export const SECONDS_A_MONTH = SECONDS_A_DAY * 30 6 | export const SECONDS_A_YEAR = SECONDS_A_DAY * 365 7 | 8 | export const MILLISECONDS_A_SECOND = 1000 9 | export const MILLISECONDS_A_MINUTE = SECONDS_A_MINUTE * MILLISECONDS_A_SECOND 10 | export const MILLISECONDS_A_HOUR = SECONDS_A_HOUR * MILLISECONDS_A_SECOND 11 | export const MILLISECONDS_A_DAY = SECONDS_A_DAY * MILLISECONDS_A_SECOND 12 | export const MILLISECONDS_A_WEEK = SECONDS_A_WEEK * MILLISECONDS_A_SECOND 13 | -------------------------------------------------------------------------------- /frontend/src/constants/linkShareHash.ts: -------------------------------------------------------------------------------- 1 | export const LINK_SHARE_HASH_PREFIX = '#share-auth-token=' 2 | -------------------------------------------------------------------------------- /frontend/src/constants/priorities.ts: -------------------------------------------------------------------------------- 1 | export const PRIORITIES = { 2 | 'UNSET': 0, 3 | 'LOW': 1, 4 | 'MEDIUM': 2, 5 | 'HIGH': 3, 6 | 'URGENT': 4, 7 | 'DO_NOW': 5, 8 | } as const 9 | 10 | export type Priority = typeof PRIORITIES[keyof typeof PRIORITIES] 11 | -------------------------------------------------------------------------------- /frontend/src/constants/rights.ts: -------------------------------------------------------------------------------- 1 | export const RIGHTS = { 2 | 'READ': 0, 3 | 'READ_WRITE': 1, 4 | 'ADMIN': 2, 5 | } as const 6 | 7 | export type Right = typeof RIGHTS[keyof typeof RIGHTS] 8 | -------------------------------------------------------------------------------- /frontend/src/directives/cypress.ts: -------------------------------------------------------------------------------- 1 | import type {Directive} from 'vue' 2 | 3 | declare global { 4 | interface Window { 5 | Cypress: object; 6 | } 7 | } 8 | 9 | const cypressDirective = >{ 10 | mounted(el, {arg, value}) { 11 | const testingId = arg || value 12 | if ((window.Cypress || import.meta.env.DEV) && testingId) { 13 | el.setAttribute('data-cy', testingId) 14 | } 15 | }, 16 | beforeUnmount(el) { 17 | el.removeAttribute('data-cy') 18 | }, 19 | } 20 | 21 | export default cypressDirective 22 | -------------------------------------------------------------------------------- /frontend/src/directives/focus.ts: -------------------------------------------------------------------------------- 1 | import type {Directive} from 'vue' 2 | 3 | const focus = >{ 4 | // When the bound element is inserted into the DOM... 5 | mounted(el, {modifiers}) { 6 | // Focus the element only if the viewport is big enough 7 | // auto focusing elements on mobile can be annoying since in these cases the 8 | // keyboard always pops up and takes half of the available space on the screen. 9 | // The threshhold is the same as the breakpoints in css. 10 | if (window.innerWidth > 769 || modifiers?.always) { 11 | el.focus() 12 | } 13 | }, 14 | } 15 | 16 | export default focus 17 | -------------------------------------------------------------------------------- /frontend/src/directives/shortcut.ts: -------------------------------------------------------------------------------- 1 | import type {Directive} from 'vue' 2 | import {install, uninstall} from '@github/hotkey' 3 | 4 | const directive = >{ 5 | mounted(el, {value}) { 6 | if(value === '') { 7 | return 8 | } 9 | install(el, value) 10 | }, 11 | beforeUnmount(el) { 12 | uninstall(el) 13 | }, 14 | } 15 | 16 | export default directive 17 | -------------------------------------------------------------------------------- /frontend/src/helpers/calculateItemPosition.ts: -------------------------------------------------------------------------------- 1 | export const calculateItemPosition = ( 2 | positionBefore: number | null = null, 3 | positionAfter: number | null = null, 4 | ): number => { 5 | if (positionBefore === null) { 6 | if (positionAfter === null) { 7 | return 0 8 | } 9 | 10 | // If there is no task after it, we just add 2^16 to the last position to have enough room in the future 11 | return positionAfter / 2 12 | } 13 | 14 | // If there is no task after it, we just add 2^16 to the last position to have enough room in the future 15 | if (positionAfter === null) { 16 | return positionBefore + Math.pow(2, 16) 17 | } 18 | 19 | // If we have both a task before and after it, we actually calculate the position 20 | return positionBefore + (positionAfter - positionBefore) / 2 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/helpers/calculateTaskPosition.test.ts: -------------------------------------------------------------------------------- 1 | import {it, expect} from 'vitest' 2 | 3 | import {calculateItemPosition} from './calculateItemPosition' 4 | 5 | it('should calculate the task position', () => { 6 | const result = calculateItemPosition(10, 100) 7 | expect(result).toBe(55) 8 | }) 9 | it('should return 0 if no position was provided', () => { 10 | const result = calculateItemPosition(null, null) 11 | expect(result).toBe(0) 12 | }) 13 | it('should calculate the task position for the first task', () => { 14 | const result = calculateItemPosition(null, 100) 15 | expect(result).toBe(50) 16 | }) 17 | it('should calculate the task position for the last task', () => { 18 | const result = calculateItemPosition(10, null) 19 | expect(result).toBe(65546) 20 | }) 21 | -------------------------------------------------------------------------------- /frontend/src/helpers/closeWhenClickedOutside.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Calls the close callback when a click happened outside of the rootElement. 3 | * 4 | * @param event The "click" event object. 5 | * @param rootElement 6 | * @param closeCallback A closure function to call when the click event happened outside of the rootElement. 7 | */ 8 | export const closeWhenClickedOutside = (event: MouseEvent, rootElement: HTMLElement, closeCallback: () => void) => { 9 | // We walk up the tree to see if any parent of the clicked element is the root element. 10 | // If it is not, we call the close callback. We're doing all this hassle to only call the 11 | // closing callback when a click happens outside of the rootElement. 12 | let parent = (event.target as HTMLElement)?.parentElement 13 | while (parent !== rootElement) { 14 | if (parent === null || parent.parentElement === null) { 15 | parent = null 16 | break 17 | } 18 | 19 | parent = parent.parentElement 20 | } 21 | 22 | if (parent === rootElement) { 23 | return 24 | } 25 | 26 | closeCallback() 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/helpers/color/colorFromHex.test.ts: -------------------------------------------------------------------------------- 1 | import {test, expect} from 'vitest' 2 | 3 | import {colorFromHex} from './colorFromHex' 4 | 5 | test('hex', () => { 6 | const color = '#ffffff' 7 | expect(colorFromHex(color)).toBe('ffffff') 8 | }) 9 | 10 | test('no hex', () => { 11 | const color = 'ffffff' 12 | expect(colorFromHex(color)).toBe('ffffff') 13 | }) 14 | -------------------------------------------------------------------------------- /frontend/src/helpers/color/colorFromHex.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the hex color code without the '#' if it has one. 3 | * 4 | * @param color 5 | * @returns {string} 6 | */ 7 | export function colorFromHex(color: string): string { 8 | if (color !== '' && color.substring(0, 1) === '#') { 9 | color = color.substring(1, 7) 10 | } 11 | 12 | return color 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/helpers/color/colorIsDark.test.ts: -------------------------------------------------------------------------------- 1 | import {test, expect} from 'vitest' 2 | 3 | import {colorIsDark} from './colorIsDark' 4 | 5 | test('dark color', () => { 6 | const color = '#111111' 7 | expect(colorIsDark(color)).toBe(false) 8 | }) 9 | 10 | test('light color', () => { 11 | const color = '#DDDDDD' 12 | expect(colorIsDark(color)).toBe(true) 13 | }) 14 | 15 | test('default dark', () => { 16 | const color = '' 17 | expect(colorIsDark(color)).toBe(true) 18 | }) 19 | -------------------------------------------------------------------------------- /frontend/src/helpers/color/colorIsDark.ts: -------------------------------------------------------------------------------- 1 | export function colorIsDark(color: string | undefined) { 2 | if (typeof color === 'undefined') { 3 | return true // Defaults to dark 4 | } 5 | 6 | if (color === '#' || color === '') { 7 | return true // Defaults to dark 8 | } 9 | 10 | if (color.substring(0, 1) !== '#') { 11 | color = '#' + color 12 | } 13 | 14 | const rgb = parseInt(color.substring(1, 7), 16) // convert rrggbb to decimal 15 | const r = (rgb >> 16) & 0xff // extract red 16 | const g = (rgb >> 8) & 0xff // extract green 17 | const b = (rgb >> 0) & 0xff // extract blue 18 | 19 | // this is a quick and dirty implementation of the WCAG 3.0 APCA color contrast formula 20 | // see: https://gist.github.com/Myndex/e1025706436736166561d339fd667493#andys-shortcut-to-luminance--lightness 21 | const Ys = Math.pow(r/255.0,2.2) * 0.2126 + 22 | Math.pow(g/255.0,2.2) * 0.7152 + 23 | Math.pow(b/255.0,2.2) * 0.0722 24 | 25 | return Math.pow(Ys,0.678) >= 0.5 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/helpers/color/randomColor.ts: -------------------------------------------------------------------------------- 1 | 2 | const COLORS = [ 3 | '#ffbe0b', 4 | '#fd8a09', 5 | '#fb5607', 6 | '#ff006e', 7 | '#efbdeb', 8 | '#8338ec', 9 | '#5f5ff6', 10 | '#3a86ff', 11 | '#4c91ff', 12 | '#0ead69', 13 | '#25be8b', 14 | '#073b4c', 15 | '#373f47', 16 | ] 17 | 18 | export function getRandomColorHex(): string { 19 | return COLORS[Math.floor(Math.random() * COLORS.length)] 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/helpers/createAsyncComponent.ts: -------------------------------------------------------------------------------- 1 | import { defineAsyncComponent, type AsyncComponentLoader, type AsyncComponentOptions, type Component, type ComponentPublicInstance } from 'vue' 2 | 3 | import ErrorComponent from '@/components/misc/Error.vue' 4 | import LoadingComponent from '@/components/misc/Loading.vue' 5 | 6 | const DEFAULT_TIMEOUT = 60000 7 | 8 | export function createAsyncComponent(source: AsyncComponentLoader | AsyncComponentOptions): T { 11 | if (typeof source === 'function') { 12 | source = { loader: source } 13 | } 14 | 15 | return defineAsyncComponent({ 16 | ...source, 17 | loadingComponent: LoadingComponent, 18 | errorComponent: ErrorComponent, 19 | timeout: DEFAULT_TIMEOUT, 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/helpers/downloadBlob.ts: -------------------------------------------------------------------------------- 1 | export const downloadBlob = (url: string, filename: string) => { 2 | const link = document.createElement('a') 3 | link.href = url 4 | link.setAttribute('download', filename) 5 | link.click() 6 | window.URL.revokeObjectURL(url) 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/helpers/editorContentEmpty.ts: -------------------------------------------------------------------------------- 1 | export function isEditorContentEmpty(content: string): boolean { 2 | return content === '' || content === '

' 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/helpers/fetcher.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import {getToken} from '@/helpers/auth' 3 | 4 | export function HTTPFactory() { 5 | const instance = axios.create({baseURL: window.API_URL}) 6 | 7 | instance.interceptors.request.use((config) => { 8 | // by setting the baseURL fresh for every request 9 | // we make sure that it is never outdated in case it is updated 10 | config.baseURL = window.API_URL 11 | 12 | return config 13 | }) 14 | 15 | return instance 16 | } 17 | 18 | export function AuthenticatedHTTPFactory() { 19 | const instance = HTTPFactory() 20 | 21 | instance.interceptors.request.use((config) => { 22 | config.headers = { 23 | ...config.headers, 24 | 'Content-Type': 'application/json', 25 | } 26 | 27 | // Set the default auth header if we have a token 28 | const token = getToken() 29 | if (token !== null) { 30 | config.headers['Authorization'] = `Bearer ${token}` 31 | } 32 | return config 33 | }) 34 | 35 | return instance 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/helpers/getBlobFromBlurHash.ts: -------------------------------------------------------------------------------- 1 | import {decode} from 'blurhash' 2 | 3 | export async function getBlobFromBlurHash(blurHash: string): Promise { 4 | if (blurHash === '') { 5 | return null 6 | } 7 | 8 | const pixels = decode(blurHash, 32, 32) 9 | const canvas = document.createElement('canvas') 10 | canvas.width = 32 11 | canvas.height = 32 12 | const ctx = canvas.getContext('2d') 13 | if (ctx === null) { 14 | return null 15 | } 16 | 17 | const imageData = ctx.createImageData(32, 32) 18 | imageData.data.set(pixels) 19 | ctx.putImageData(imageData, 0, 0) 20 | 21 | return new Promise((resolve, reject) => { 22 | canvas.toBlob(b => { 23 | if (b === null) { 24 | reject(b) 25 | return 26 | } 27 | 28 | resolve(b) 29 | }) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/helpers/getFullBaseUrl.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get full BASE_URL 3 | * - including path 4 | * - will always end with a trailing slash 5 | */ 6 | export function getFullBaseUrl() { 7 | // (1) The injected BASE_URL is declared from the `resolvedBase` that might miss a trailing slash... 8 | // see: https://github.com/vitejs/vite/blob/b35fe883fdc699ac1450882562872095abe9959b/packages/vite/src/node/config.ts#LL614C25-L614C25 9 | const rawBase = import.meta.env.BASE_URL 10 | // (2) so we readd a slash like done here 11 | // https://github.com/vitejs/vite/blob/b35fe883fdc699ac1450882562872095abe9959b/packages/vite/src/node/config.ts#L643 12 | // See this comment: https://github.com/vitejs/vite/pull/10723#issuecomment-1303627478 13 | return rawBase.endsWith('/') ? rawBase : rawBase + '/' 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/helpers/getHumanSize.ts: -------------------------------------------------------------------------------- 1 | const SIZES = [ 2 | 'B', 3 | 'KB', 4 | 'MB', 5 | 'GB', 6 | 'TB', 7 | ] as const 8 | 9 | export function getHumanSize(inputSize: number) { 10 | let iterator = 0 11 | let size = inputSize 12 | while (size > 1024) { 13 | size /= 1024 14 | iterator++ 15 | } 16 | 17 | return Number(Math.round(Number(size + 'e2')) + 'e-2') + ' ' + SIZES[iterator] 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/helpers/getInheritedBackgroundColor.ts: -------------------------------------------------------------------------------- 1 | function getDefaultBackground() { 2 | const div = document.createElement('div') 3 | document.head.appendChild(div) 4 | const bg = window.getComputedStyle(div).backgroundColor 5 | document.head.removeChild(div) 6 | return bg 7 | } 8 | 9 | // get default style for current browser 10 | const defaultStyle = getDefaultBackground() // typically "rgba(0, 0, 0, 0)" 11 | 12 | // based on https://stackoverflow.com/a/62630563/15522256 13 | export function getInheritedBackgroundColor(el: HTMLElement): string { 14 | const backgroundColor = window.getComputedStyle(el).backgroundColor 15 | 16 | if (backgroundColor !== defaultStyle) return backgroundColor 17 | 18 | if (!el.parentElement) { 19 | // we reached the top parent el without getting an explicit color 20 | return defaultStyle 21 | } 22 | 23 | return getInheritedBackgroundColor(el.parentElement) 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/helpers/getProjectTitle.ts: -------------------------------------------------------------------------------- 1 | import {i18n} from '@/i18n' 2 | import type {IProject} from '@/modelTypes/IProject' 3 | 4 | export function getProjectTitle(project: IProject) { 5 | if (project.id === -1) { 6 | return i18n.global.t('project.pseudo.favorites.title') 7 | } 8 | 9 | if (project.title === 'Inbox') { 10 | return i18n.global.t('project.inboxTitle') 11 | } 12 | 13 | return project.title 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/helpers/hourToDaytime.ts: -------------------------------------------------------------------------------- 1 | import type { Daytime } from '@/composables/useDaytimeSalutation' 2 | 3 | export function hourToDaytime(now: Date): Daytime { 4 | const hours = now.getHours() 5 | 6 | const daytimeMap = { 7 | night: hours < 5 || hours > 23, 8 | morning: hours < 11, 9 | day: hours < 18, 10 | evening: hours < 23, 11 | } as Record 12 | 13 | return (Object.keys(daytimeMap) as Daytime[]).find((daytime) => daytimeMap[daytime]) || 'night' 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/helpers/isAppleDevice.ts: -------------------------------------------------------------------------------- 1 | export const isAppleDevice = (): boolean => { 2 | return navigator.userAgent.includes('Mac') || [ 3 | 'iPad Simulator', 4 | 'iPhone Simulator', 5 | 'iPod Simulator', 6 | 'iPad', 7 | 'iPhone', 8 | 'iPod', 9 | ].includes(navigator.platform) 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/helpers/isEmail.ts: -------------------------------------------------------------------------------- 1 | export function isEmail(email: string): boolean { 2 | const format = /^.+@.+$/ 3 | const match = email.match(format) 4 | 5 | return match === null ? false : match.length > 0 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/helpers/isValidHttpUrl.ts: -------------------------------------------------------------------------------- 1 | export function isValidHttpUrl(urlToCheck: string): boolean { 2 | let url 3 | 4 | try { 5 | url = new URL(urlToCheck) 6 | } catch (_) { 7 | return false 8 | } 9 | 10 | return url.protocol === 'http:' || url.protocol === 'https:' 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/helpers/parseDateOrNull.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Make date objects from timestamps 3 | */ 4 | export function parseDateOrNull(date: string | Date) { 5 | if (date instanceof Date) { 6 | return date 7 | } 8 | 9 | if ((typeof date === 'string') && !date.startsWith('0001')) { 10 | return new Date(date) 11 | } 12 | 13 | return null 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/helpers/playPop.ts: -------------------------------------------------------------------------------- 1 | import {useAuthStore} from '@/stores/auth' 2 | 3 | import popSoundFile from '@/assets/audio/pop.mp3' 4 | 5 | export function playPopSound() { 6 | const playSoundWhenDone = useAuthStore().settings.frontendSettings.playSoundWhenDone 7 | 8 | if (!playSoundWhenDone) 9 | return 10 | 11 | try { 12 | const popSound = new Audio(popSoundFile) 13 | popSound.play() 14 | } catch (e) { 15 | console.error('Could not play pop sound:', e) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/helpers/randomId.ts: -------------------------------------------------------------------------------- 1 | const DEFAULT_ID_LENGTH = 9 2 | 3 | export function createRandomID(idLength = DEFAULT_ID_LENGTH) { 4 | return Math.random().toString(36).slice(2, idLength) 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/helpers/replaceAll.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This function replaces all text, no matter the case. 3 | * 4 | * See https://stackoverflow.com/a/7313467/10924593 5 | * 6 | * @parma str 7 | * @param search 8 | * @param replace 9 | * @returns {*} 10 | */ 11 | export const replaceAll = (str: string, search: string, replace: string) => { 12 | const esc = search.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') 13 | const reg = new RegExp(esc, 'ig') 14 | return str.replace(reg, replace) 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/helpers/saveCollapsedBucketState.ts: -------------------------------------------------------------------------------- 1 | import type {IBucket} from '@/modelTypes/IBucket' 2 | import type {IProject} from '@/modelTypes/IProject' 3 | 4 | const key = 'collapsedBuckets' 5 | 6 | export type CollapsedBuckets = {[id: IBucket['id']]: boolean} 7 | 8 | function getAllState() { 9 | const saved = localStorage.getItem(key) 10 | return saved === null 11 | ? {} 12 | : JSON.parse(saved) 13 | } 14 | 15 | export const saveCollapsedBucketState = ( 16 | projectId: IProject['id'], 17 | collapsedBuckets: CollapsedBuckets, 18 | ) => { 19 | const state = getAllState() 20 | state[projectId] = collapsedBuckets 21 | for (const bucketId in state[projectId]) { 22 | if (!state[projectId][bucketId]) { 23 | delete state[projectId][bucketId] 24 | } 25 | } 26 | localStorage.setItem(key, JSON.stringify(state)) 27 | } 28 | 29 | export function getCollapsedBucketState(projectId : IProject['id']) { 30 | const state = getAllState() 31 | return typeof state[projectId] !== 'undefined' 32 | ? state[projectId] 33 | : {} 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/helpers/saveLastVisited.ts: -------------------------------------------------------------------------------- 1 | const LAST_VISITED_KEY = 'lastVisited' 2 | 3 | export const saveLastVisited = (name: string | undefined, params: object, query: object) => { 4 | if (typeof name === 'undefined') { 5 | return 6 | } 7 | 8 | localStorage.setItem(LAST_VISITED_KEY, JSON.stringify({name, params, query})) 9 | } 10 | 11 | export const getLastVisited = () => { 12 | const lastVisited = localStorage.getItem(LAST_VISITED_KEY) 13 | if (lastVisited === null) { 14 | return null 15 | } 16 | 17 | return JSON.parse(lastVisited) 18 | } 19 | 20 | export const clearLastVisited = () => { 21 | return localStorage.removeItem(LAST_VISITED_KEY) 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/helpers/scrollIntoView.ts: -------------------------------------------------------------------------------- 1 | export function scrollIntoView(el: HTMLElement | null | undefined) { 2 | if (!el) { 3 | return 4 | } 5 | 6 | const boundingRect = el.getBoundingClientRect() 7 | const scrollY = window.scrollY 8 | 9 | if ( 10 | boundingRect.top > (scrollY + window.innerHeight) || 11 | boundingRect.top < scrollY 12 | ) { 13 | el.scrollIntoView({ 14 | behavior: 'smooth', 15 | block: 'center', 16 | inline: 'nearest', 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/helpers/setTitle.ts: -------------------------------------------------------------------------------- 1 | export function setTitle(title : undefined | string) { 2 | document.title = (typeof title === 'undefined' || title === '') 3 | ? 'Vikunja' 4 | : `${title} | Vikunja` 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/helpers/time/calculateDayInterval.ts: -------------------------------------------------------------------------------- 1 | type Day = T 2 | 3 | export function calculateDayInterval(dateString: string, currentDay = (new Date().getDay())): Day { 4 | switch (dateString) { 5 | case 'today': 6 | return 0 7 | case 'tomorrow': 8 | return 1 9 | case 'nextMonday': 10 | // Monday is 1, so we calculate the distance to the next 1 11 | return (currentDay + (8 - currentDay * 2)) % 7 12 | case 'thisWeekend': 13 | // Saturday is 6 so we calculate the distance to the next 6 14 | return (6 - currentDay) % 6 15 | case 'laterThisWeek': 16 | if (currentDay === 5 || currentDay === 6 || currentDay === 0) { 17 | return 0 18 | } 19 | 20 | return 2 21 | case 'laterNextWeek': 22 | return calculateDayInterval('laterThisWeek', currentDay) + 7 23 | case 'nextWeek': 24 | return 7 25 | default: 26 | return 0 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/helpers/time/calculateNearestHours.ts: -------------------------------------------------------------------------------- 1 | export function calculateNearestHours(currentDate: Date = new Date()): number { 2 | if (currentDate.getHours() <= 9 || currentDate.getHours() > 21) { 3 | return 9 4 | } 5 | 6 | if (currentDate.getHours() <= 12) { 7 | return 12 8 | } 9 | 10 | if (currentDate.getHours() <= 15) { 11 | return 15 12 | } 13 | 14 | if (currentDate.getHours() <= 18) { 15 | return 18 16 | } 17 | 18 | if (currentDate.getHours() <= 21) { 19 | return 21 20 | } 21 | 22 | // Same case as in the first if, will never be called 23 | return 9 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/helpers/time/createDateFromString.test.ts: -------------------------------------------------------------------------------- 1 | import {test, expect} from 'vitest' 2 | 3 | import {createDateFromString} from './createDateFromString' 4 | 5 | test('YYYY-MM-DD HH:MM', () => { 6 | const dateString = '2021-02-06 12:00' 7 | const date = createDateFromString(dateString) 8 | expect(date).toBeInstanceOf(Date) 9 | expect(date.getDate()).toBe(6) 10 | expect(date.getMonth()).toBe(1) 11 | expect(date.getFullYear()).toBe(2021) 12 | expect(date.getHours()).toBe(12) 13 | expect(date.getMinutes()).toBe(0) 14 | expect(date.getSeconds()).toBe(0) 15 | }) 16 | -------------------------------------------------------------------------------- /frontend/src/helpers/time/createDateFromString.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a new date from any format in a way that all browsers, especially safari, can understand. 3 | * 4 | * @see https://kolaente.dev/vikunja/frontend/issues/207 5 | * 6 | * @param dateString 7 | * @returns {Date} 8 | */ 9 | export function createDateFromString(dateString: string | Date) { 10 | if (dateString instanceof Date) { 11 | return dateString 12 | } 13 | 14 | if (dateString.includes('-')) { 15 | dateString = dateString.replace(/-/g, '/') 16 | } 17 | 18 | return new Date(dateString) 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/helpers/time/getNextWeekDate.ts: -------------------------------------------------------------------------------- 1 | import {MILLISECONDS_A_WEEK} from '@/constants/date' 2 | 3 | export function getNextWeekDate(): Date { 4 | return new Date((new Date()).getTime() + MILLISECONDS_A_WEEK) 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/helpers/time/isoToKebabDate.ts: -------------------------------------------------------------------------------- 1 | import type {DateISO} from '@/types/DateISO' 2 | import type {DateKebab} from '@/types/DateKebab' 3 | 4 | // ✅ Format a date to YYYY-MM-DD (or any other format) 5 | function padTo2Digits(num: number) { 6 | return num.toString().padStart(2, '0') 7 | } 8 | 9 | export function isoToKebabDate(isoDate: DateISO) { 10 | const date = new Date(isoDate) 11 | return [ 12 | date.getFullYear(), 13 | padTo2Digits(date.getMonth() + 1), // January is 0, but we want it to be 1 14 | padTo2Digits(date.getDate()), 15 | ].join('-') as DateKebab 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/helpers/time/parseBooleanProp.ts: -------------------------------------------------------------------------------- 1 | export function parseBooleanProp(booleanProp: string | undefined) { 2 | return (booleanProp === 'false' || booleanProp === '0') 3 | ? false 4 | : Boolean(booleanProp) 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/helpers/time/parseDateOrString.ts: -------------------------------------------------------------------------------- 1 | export function parseDateOrString(rawValue: string | undefined | null, fallback: unknown): (unknown | string | Date) { 2 | if (rawValue === null || typeof rawValue === 'undefined') { 3 | return fallback 4 | } 5 | 6 | if (rawValue.toLowerCase().includes('now') || rawValue.toLowerCase().includes('||')) { 7 | return rawValue 8 | } 9 | 10 | const d = new Date(rawValue) 11 | 12 | return !isNaN(+d) 13 | ? d 14 | : rawValue 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/helpers/time/parseDateProp.ts: -------------------------------------------------------------------------------- 1 | import type {DateISO} from '@/types/DateISO' 2 | import type {DateKebab} from '@/types/DateKebab' 3 | 4 | export function parseDateProp(kebabDate: DateKebab | undefined): string | undefined { 5 | try { 6 | 7 | if (!kebabDate) { 8 | throw new Error('No value') 9 | } 10 | const dateValues = kebabDate.split('-') 11 | const [, monthString, dateString] = dateValues 12 | const [year, month, date] = dateValues.map(val => Number(val)) 13 | const dateValuesAreValid = ( 14 | !Number.isNaN(year) && 15 | monthString.length >= 1 && monthString.length <= 2 && 16 | !Number.isNaN(month) && 17 | month >= 1 && month <= 12 && 18 | dateString.length >= 1 && dateString.length <= 31 && 19 | !Number.isNaN(date) && 20 | date >= 1 && date <= 31 21 | ) 22 | if (!dateValuesAreValid) { 23 | throw new Error('Invalid date values') 24 | } 25 | return new Date(year, month - 1, date).toISOString() as DateISO 26 | } catch(_) { 27 | // ignore nonsense route queries 28 | return 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/helpers/useFlatpickrLanguage.ts: -------------------------------------------------------------------------------- 1 | import {useAuthStore} from '@/stores/auth' 2 | // TODO: only import needed languages 3 | import FlatpickrLanguages from 'flatpickr/dist/l10n' 4 | import type { key } from 'flatpickr/dist/types/locale' 5 | import { computed } from 'vue' 6 | 7 | export function useFlatpickrLanguage() { 8 | const authStore = useAuthStore() 9 | 10 | return computed(() => { 11 | const userLanguage = authStore.settings.language 12 | if (!userLanguage) { 13 | return FlatpickrLanguages.en 14 | } 15 | 16 | const langPair = userLanguage.split('-') 17 | const code = userLanguage === 'vi-VN' ? 'vn' : 'en' 18 | const language = FlatpickrLanguages?.[langPair?.[0] as key] || FlatpickrLanguages[code] 19 | language.firstDayOfWeek = authStore.settings.weekStart ?? language.firstDayOfWeek 20 | return language 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | export function findIndexById(array : T[], id : string | number) { 2 | return array.findIndex(({id: currentId}) => currentId === id) 3 | } 4 | 5 | export function findById(array : T[], id : string | number) { 6 | return array.find(({id: currentId}) => currentId === id) 7 | } 8 | 9 | interface ObjectWithId { 10 | id: number 11 | } 12 | 13 | export function includesById(array: ObjectWithId[], id: string | number) { 14 | return array.some(({id: currentId}) => currentId === id) 15 | } 16 | 17 | // https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_isnil 18 | export function isNil(value: unknown) { 19 | return value == null 20 | } 21 | 22 | export function omitBy(obj: Record, check: (value: unknown) => boolean) { 23 | if (isNil(obj)) { 24 | return {} 25 | } 26 | 27 | return Object.fromEntries( 28 | Object.entries(obj).filter(([, value]) => !check(value)), 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/helpers/validatePasswort.ts: -------------------------------------------------------------------------------- 1 | export function validatePassword(password: string, validateMinLength: boolean = true): string | true { 2 | if (password === '') { 3 | return 'user.auth.passwordRequired' 4 | } 5 | 6 | if (validateMinLength && password.length < 8) { 7 | return 'user.auth.passwordNotMin' 8 | } 9 | 10 | if (validateMinLength && password.length > 72) { 11 | return 'user.auth.passwordNotMax' 12 | } 13 | 14 | return true 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/histoire.setup.ts: -------------------------------------------------------------------------------- 1 | import {defineSetupVue3} from '@histoire/plugin-vue' 2 | import {i18n} from './i18n' 3 | 4 | // import './histoire.css' // Import global CSS 5 | import './styles/global.scss' 6 | 7 | import {createPinia} from 'pinia' 8 | 9 | import cypress from '@/directives/cypress' 10 | 11 | import FontAwesomeIcon from '@/components/misc/Icon' 12 | import XButton from '@/components/input/button.vue' 13 | import Modal from '@/components/misc/Modal.vue' 14 | import Card from '@/components/misc/Card.vue' 15 | 16 | export const setupVue3 = defineSetupVue3(({ app }) => { 17 | // Add Pinia store 18 | const pinia = createPinia() 19 | app.use(pinia) 20 | app.use(i18n) 21 | 22 | app.directive('cy', cypress) 23 | 24 | app.component('Icon', FontAwesomeIcon) 25 | app.component('XButton', XButton) 26 | app.component('Modal', Modal) 27 | app.component('Card', Card) 28 | }) 29 | -------------------------------------------------------------------------------- /frontend/src/i18n/lang/ca-ES.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /frontend/src/i18n/lang/eo-UY.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /frontend/src/i18n/lang/ro-RO.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /frontend/src/i18n/lang/sk-SK.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /frontend/src/i18n/lang/sr-CS.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /frontend/src/i18n/lang/zh-TW.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /frontend/src/modelTypes/IAbstract.ts: -------------------------------------------------------------------------------- 1 | import type {Right} from '@/constants/rights' 2 | 3 | export interface IAbstract { 4 | maxRight: Right | null // FIXME: should this be readonly? 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/IApiToken.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from '@/modelTypes/IAbstract' 2 | 3 | export interface IApiPermission { 4 | [key: string]: string[] 5 | } 6 | 7 | export interface IApiToken extends IAbstract { 8 | id: number 9 | title: string 10 | token: string 11 | permissions: IApiPermission 12 | expiresAt: Date 13 | created: Date 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/IAttachment.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | import type {IFile} from './IFile' 3 | import type {IUser} from './IUser' 4 | 5 | export interface IAttachment extends IAbstract { 6 | id: number 7 | taskId: number 8 | createdBy: IUser 9 | file: IFile 10 | created: Date 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/IAvatar.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | 3 | export type AvatarProvider = 'default' | 'initials' | 'gravatar' | 'marble' | 'upload' 4 | 5 | export interface IAvatar extends IAbstract { 6 | avatarProvider: AvatarProvider 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/IBackgroundImage.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | 3 | export interface IBackgroundImage extends IAbstract { 4 | id: number 5 | url: string 6 | thumb: string 7 | info: { 8 | author: string 9 | authorName: string 10 | } 11 | blurHash: string 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/IBucket.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | import type {IUser} from './IUser' 3 | import type {ITask} from './ITask' 4 | import type {IProjectView} from '@/modelTypes/IProjectView' 5 | 6 | export interface IBucket extends IAbstract { 7 | id: number 8 | title: string 9 | projectId: number 10 | limit: number 11 | tasks: ITask[] 12 | position: number 13 | count: number 14 | projectViewId: IProjectView['id'] 15 | 16 | createdBy: IUser 17 | created: Date 18 | updated: Date 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/ICaldavToken.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | 3 | export interface ICaldavToken extends IAbstract { 4 | id: number; 5 | 6 | created: Date; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/IEmailUpdate.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | 3 | export interface IEmailUpdate extends IAbstract { 4 | newEmail: string 5 | password: string 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/IFile.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | 3 | export interface IFile extends IAbstract { 4 | id: number 5 | mime: string 6 | name: string 7 | size: number 8 | 9 | created: Date 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/ILabel.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | import type {IUser} from './IUser' 3 | 4 | export interface ILabel extends IAbstract { 5 | id: number 6 | title: string 7 | hexColor: string 8 | description: string 9 | createdBy: IUser 10 | projectId: number 11 | textColor: string 12 | 13 | created: Date 14 | updated: Date 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/ILabelTask.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | 3 | export interface ILabelTask extends IAbstract { 4 | id: number 5 | taskId: number 6 | labelId: number 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/ILinkShare.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | import type {IUser} from './IUser' 3 | import type { Right } from '@/constants/rights' 4 | 5 | export interface ILinkShare extends IAbstract { 6 | id: number 7 | hash: string 8 | right: Right 9 | sharedBy: IUser 10 | sharingType: number // FIXME: use correct numbers 11 | projectId: number 12 | name: string 13 | password: string 14 | 15 | created: Date 16 | updated: Date 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/IPasswordReset.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | 3 | export interface IPasswordReset extends IAbstract { 4 | token: string 5 | newPassword: string 6 | email: string 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/IPasswordUpdate.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | 3 | export interface IPasswordUpdate extends IAbstract { 4 | newPassword: string 5 | oldPassword: string 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/IProject.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | import type {ITask} from './ITask' 3 | import type {IUser} from './IUser' 4 | import type {ISubscription} from './ISubscription' 5 | import type {IProjectView} from '@/modelTypes/IProjectView' 6 | 7 | 8 | export interface IProject extends IAbstract { 9 | id: number 10 | title: string 11 | description: string 12 | owner: IUser 13 | tasks: ITask[] 14 | isArchived: boolean 15 | hexColor: string 16 | identifier: string 17 | backgroundInformation: unknown | null // FIXME: improve type 18 | isFavorite: boolean 19 | subscription: ISubscription 20 | position: number 21 | backgroundBlurHash: string 22 | parentProjectId: number 23 | views: IProjectView[] 24 | 25 | created: Date 26 | updated: Date 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/IProjectDuplicate.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | import type {IProject} from './IProject' 3 | 4 | export interface IProjectDuplicate extends IAbstract { 5 | projectId: number 6 | duplicatedProject: IProject | null 7 | parentProjectId: IProject['id'] 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/IReaction.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from '@/modelTypes/IAbstract' 2 | import type {IUser} from '@/modelTypes/IUser' 3 | 4 | export type ReactionKind = 'tasks' | 'comments' 5 | 6 | export interface IReaction extends IAbstract { 7 | id: number 8 | kind: ReactionKind 9 | value: string 10 | } 11 | 12 | export interface IReactionPerEntity { 13 | [reaction: string]: IUser[] 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/ISavedFilter.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | import type {IUser} from './IUser' 3 | 4 | // FIXME: what makes this different from TaskFilterParams? 5 | export interface IFilters { 6 | sort_by: ('start_date' | 'done' | 'id' | 'position')[], 7 | order_by: ('asc' | 'desc')[], 8 | filter: string, 9 | filter_include_nulls: boolean, 10 | s: string, 11 | } 12 | 13 | export interface ISavedFilter extends IAbstract { 14 | id: number 15 | title: string 16 | description: string 17 | filters: IFilters 18 | 19 | owner: IUser 20 | created: Date 21 | updated: Date 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/ISubscription.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | import type {IUser} from './IUser' 3 | 4 | export interface ISubscription extends IAbstract { 5 | id: number 6 | entity: string // FIXME: correct type? 7 | entityId: number // FIXME: correct type? 8 | user: IUser 9 | 10 | created: Date 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/ITaskAssignee.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | import type {ITask} from './ITask' 3 | import type {IUser} from './IUser' 4 | 5 | export interface ITaskAssignee extends IAbstract { 6 | created: Date 7 | userId: IUser['id'] 8 | taskId: ITask['id'] 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/ITaskBucket.ts: -------------------------------------------------------------------------------- 1 | import type {IProjectView} from '@/modelTypes/IProjectView' 2 | import type {IAbstract} from '@/modelTypes/IAbstract' 3 | import type {IBucket} from '@/modelTypes/IBucket' 4 | import type {ITask} from '@/modelTypes/ITask' 5 | import type {IProject} from '@/modelTypes/IProject' 6 | 7 | export interface ITaskBucket extends IAbstract { 8 | taskId: ITask['id'] 9 | bucketId: IBucket['id'] 10 | projectViewId: IProjectView['id'] 11 | projectId: IProject['id'] 12 | task: ?ITask 13 | bucket: ?IBucket 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/ITaskComment.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | import type {IUser} from './IUser' 3 | import type {ITask} from './ITask' 4 | import type {IReactionPerEntity} from '@/modelTypes/IReaction' 5 | 6 | export interface ITaskComment extends IAbstract { 7 | id: number 8 | taskId: ITask['id'] 9 | comment: string 10 | author: IUser 11 | 12 | reactions: IReactionPerEntity 13 | 14 | created: Date 15 | updated: Date 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/ITaskPosition.ts: -------------------------------------------------------------------------------- 1 | import type {IProjectView} from '@/modelTypes/IProjectView' 2 | import type {IAbstract} from '@/modelTypes/IAbstract' 3 | 4 | export interface ITaskPosition extends IAbstract { 5 | position: number 6 | projectViewId: IProjectView['id'] 7 | taskId: number 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/ITaskRelation.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | import type {IUser} from './IUser' 3 | import type {ITask} from './ITask' 4 | 5 | import type {IRelationKind} from '@/types/IRelationKind' 6 | 7 | export interface ITaskRelation extends IAbstract { 8 | id: number 9 | otherTaskId: ITask['id'] 10 | taskId: ITask['id'] 11 | relationKind: IRelationKind 12 | 13 | createdBy: IUser 14 | created: Date 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/ITaskReminder.ts: -------------------------------------------------------------------------------- 1 | import type { IAbstract } from './IAbstract' 2 | import type { IReminderPeriodRelativeTo } from '@/types/IReminderPeriodRelativeTo' 3 | 4 | export interface ITaskReminder extends IAbstract { 5 | reminder: Date | null 6 | relativePeriod: number 7 | relativeTo: IReminderPeriodRelativeTo | null 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/ITeam.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | import type {IUser} from './IUser' 3 | import type {ITeamMember} from './ITeamMember' 4 | import type {Right} from '@/constants/rights' 5 | 6 | export interface ITeam extends IAbstract { 7 | id: number 8 | name: string 9 | description: string 10 | members: ITeamMember[] 11 | right: Right 12 | externalId: string 13 | isPublic: boolean 14 | 15 | createdBy: IUser 16 | created: Date 17 | updated: Date 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/ITeamMember.ts: -------------------------------------------------------------------------------- 1 | import type {IUser} from './IUser' 2 | import type {IProject} from './IProject' 3 | 4 | export interface ITeamMember extends IUser { 5 | admin: boolean 6 | teamId: IProject['id'] 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/ITeamProject.ts: -------------------------------------------------------------------------------- 1 | import type {ITeamShareBase} from './ITeamShareBase' 2 | import type {IProject} from './IProject' 3 | 4 | export interface ITeamProject extends ITeamShareBase { 5 | projectId: IProject['id'] 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/ITeamShareBase.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | import type {ITeam} from './ITeam' 3 | import type {Right} from '@/constants/rights' 4 | 5 | export interface ITeamShareBase extends IAbstract { 6 | teamId: ITeam['id'] 7 | right: Right 8 | 9 | created: Date 10 | updated: Date 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/ITotp.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | 3 | export interface ITotp extends IAbstract { 4 | secret: string 5 | enabled: boolean 6 | url: string 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/IUser.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | import type {IUserSettings} from './IUserSettings' 3 | 4 | export const AUTH_TYPES = { 5 | 'UNKNOWN': 0, 6 | 'USER': 1, 7 | 'LINK_SHARE': 2, 8 | } as const 9 | 10 | export type AuthType = typeof AUTH_TYPES[keyof typeof AUTH_TYPES] 11 | 12 | export interface IUser extends IAbstract { 13 | id: number 14 | email: string 15 | username: string 16 | name: string 17 | exp: number 18 | type: AuthType 19 | 20 | created: Date 21 | updated: Date 22 | settings: IUserSettings 23 | 24 | isLocalUser: boolean 25 | deletionScheduledAt: string | Date | null 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/IUserProject.ts: -------------------------------------------------------------------------------- 1 | import type {IUserShareBase} from './IUserShareBase' 2 | import type {IProject} from './IProject' 3 | 4 | export interface IUserProject extends IUserShareBase { 5 | projectId: IProject['id'] 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/IUserShareBase.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | import type {IUser} from './IUser' 3 | import type {Right} from '@/constants/rights' 4 | 5 | export interface IUserShareBase extends IAbstract { 6 | userId: IUser['id'] 7 | right: Right 8 | 9 | created: Date 10 | updated: Date 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/modelTypes/IWebhook.ts: -------------------------------------------------------------------------------- 1 | import type {IAbstract} from './IAbstract' 2 | import type {IUser} from '@/modelTypes/IUser' 3 | 4 | export interface IWebhook extends IAbstract { 5 | id: number 6 | projectId: number 7 | secret: string 8 | targetUrl: string 9 | events: string[] 10 | createdBy: IUser 11 | 12 | created: Date 13 | updated: Date 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/models/abstractModel.ts: -------------------------------------------------------------------------------- 1 | import {objectToCamelCase} from '@/helpers/case' 2 | import {omitBy, isNil} from '@/helpers/utils' 3 | import type {Right} from '@/constants/rights' 4 | import type {IAbstract} from '@/modelTypes/IAbstract' 5 | 6 | export default abstract class AbstractModel implements IAbstract { 7 | 8 | 9 | /** 10 | * The max right the user has on this object, as returned by the x-max-right header from the api. 11 | */ 12 | maxRight: Right | null = null 13 | 14 | /** 15 | * Takes an object and merges its data with the default data of this model. 16 | */ 17 | assignData(data: Partial) { 18 | data = objectToCamelCase(data) 19 | 20 | // Put all data in our model while overriding those with a value of null or undefined with their defaults 21 | Object.assign(this, omitBy(data, isNil)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/models/apiTokenModel.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from '@/models/abstractModel' 2 | import type {IApiToken} from '@/modelTypes/IApiToken' 3 | 4 | export default class ApiTokenModel extends AbstractModel { 5 | id = 0 6 | title = '' 7 | token = '' 8 | permissions = null 9 | expiresAt: Date = null 10 | created: Date = null 11 | 12 | constructor(data: Partial = {}) { 13 | super() 14 | 15 | this.assignData(data) 16 | 17 | this.expiresAt = new Date(this.expiresAt) 18 | this.created = new Date(this.created) 19 | this.updated = new Date(this.updated) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/models/attachment.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | import UserModel from './user' 3 | import FileModel from './file' 4 | import type { IUser } from '@/modelTypes/IUser' 5 | import type { IFile } from '@/modelTypes/IFile' 6 | import type { IAttachment } from '@/modelTypes/IAttachment' 7 | 8 | export const SUPPORTED_IMAGE_SUFFIX = ['.jpeg', '.jpg', '.png', '.bmp', '.gif'] 9 | 10 | export function canPreview(attachment: IAttachment): boolean { 11 | return SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.toLowerCase().endsWith(suffix)) 12 | } 13 | 14 | export default class AttachmentModel extends AbstractModel implements IAttachment { 15 | id = 0 16 | taskId = 0 17 | createdBy: IUser = UserModel 18 | file: IFile = FileModel 19 | created: Date = null 20 | 21 | constructor(data: Partial) { 22 | super() 23 | this.assignData(data) 24 | 25 | this.createdBy = new UserModel(this.createdBy) 26 | this.file = new FileModel(this.file) 27 | this.created = new Date(this.created) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/models/avatar.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | import type { IAvatar } from '@/modelTypes/IAvatar' 3 | 4 | export default class AvatarModel extends AbstractModel implements IAvatar { 5 | avatarProvider: IAvatar['avatarProvider'] = 'default' 6 | 7 | constructor(data: Partial) { 8 | super() 9 | this.assignData(data) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/models/backgroundImage.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | import type {IBackgroundImage} from '@/modelTypes/IBackgroundImage' 3 | 4 | export default class BackgroundImageModel extends AbstractModel implements IBackgroundImage { 5 | id = 0 6 | url = '' 7 | thumb = '' 8 | info: { 9 | author: string 10 | authorName: string 11 | } = {} 12 | blurHash = '' 13 | 14 | constructor(data: Partial) { 15 | super() 16 | this.assignData(data) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/models/bucket.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | import UserModel from './user' 3 | import TaskModel from './task' 4 | 5 | import type {IBucket} from '@/modelTypes/IBucket' 6 | import type {ITask} from '@/modelTypes/ITask' 7 | import type {IUser} from '@/modelTypes/IUser' 8 | 9 | export default class BucketModel extends AbstractModel implements IBucket { 10 | id = 0 11 | title = '' 12 | projectId = '' 13 | limit = 0 14 | tasks: ITask[] = [] 15 | position = 0 16 | count = 0 17 | 18 | createdBy: IUser = null 19 | created: Date = null 20 | updated: Date = null 21 | 22 | constructor(data: Partial) { 23 | super() 24 | this.assignData(data) 25 | 26 | this.tasks = this.tasks.map(t => new TaskModel(t)) 27 | 28 | this.createdBy = new UserModel(this.createdBy) 29 | this.created = new Date(this.created) 30 | this.updated = new Date(this.updated) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/models/caldavToken.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | 3 | import type {ICaldavToken} from '@/modelTypes/ICaldavToken' 4 | 5 | export default class CaldavTokenModel extends AbstractModel implements ICaldavToken { 6 | id: number 7 | created: Date 8 | 9 | constructor(data: Partial) { 10 | super() 11 | this.assignData(data) 12 | 13 | if (this.created) { 14 | this.created = new Date(this.created) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/models/emailUpdate.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | 3 | import type {IEmailUpdate} from '@/modelTypes/IEmailUpdate' 4 | 5 | export default class EmailUpdateModel extends AbstractModel implements IEmailUpdate { 6 | newEmail = '' 7 | password = '' 8 | 9 | constructor(data : Partial = {}) { 10 | super() 11 | this.assignData(data) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/models/file.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | import type {IFile} from '@/modelTypes/IFile' 3 | 4 | export default class FileModel extends AbstractModel implements IFile { 5 | id = 0 6 | mime = '' 7 | name = '' 8 | size = 0 9 | created: Date = null 10 | 11 | constructor(data: Partial) { 12 | super() 13 | this.assignData(data) 14 | 15 | this.created = new Date(this.created) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/models/labelTask.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | 3 | import type { ILabelTask } from '@/modelTypes/ILabelTask' 4 | 5 | export default class LabelTask extends AbstractModel implements ILabelTask { 6 | id = 0 7 | taskId = 0 8 | labelId = 0 9 | 10 | constructor(data: Partial) { 11 | super() 12 | this.assignData(data) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/models/linkShare.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | import UserModel from './user' 3 | 4 | import {RIGHTS, type Right} from '@/constants/rights' 5 | import type {ILinkShare} from '@/modelTypes/ILinkShare' 6 | import type {IUser} from '@/modelTypes/IUser' 7 | 8 | export default class LinkShareModel extends AbstractModel implements ILinkShare { 9 | id = 0 10 | hash = '' 11 | right: Right = RIGHTS.READ 12 | sharedBy: IUser = UserModel 13 | sharingType = 0 // FIXME: use correct numbers 14 | projectId = 0 15 | name: '' 16 | password: '' 17 | created: Date = null 18 | updated: Date = null 19 | 20 | constructor(data: Partial) { 21 | super() 22 | this.assignData(data) 23 | 24 | this.sharedBy = new UserModel(this.sharedBy) 25 | 26 | this.created = new Date(this.created) 27 | this.updated = new Date(this.updated) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/models/passwordReset.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | 3 | import type {IPasswordReset} from '@/modelTypes/IPasswordReset' 4 | 5 | export default class PasswordResetModel extends AbstractModel implements IPasswordReset { 6 | token = '' 7 | newPassword = '' 8 | email = '' 9 | 10 | constructor(data: Partial = {}) { 11 | super() 12 | this.assignData(data) 13 | 14 | if (data.token) { 15 | this.token = data.token 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/models/passwordUpdate.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | 3 | import type {IPasswordUpdate} from '@/modelTypes/IPasswordUpdate' 4 | 5 | export default class PasswordUpdateModel extends AbstractModel implements IPasswordUpdate { 6 | newPassword = '' 7 | oldPassword = '' 8 | 9 | constructor(data: Partial = {}) { 10 | super() 11 | this.assignData(data) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/models/projectDuplicateModel.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | import ProjectModel from './project' 3 | 4 | import type {IProjectDuplicate} from '@/modelTypes/IProjectDuplicate' 5 | import type {IProject} from '@/modelTypes/IProject' 6 | 7 | export default class ProjectDuplicateModel extends AbstractModel implements IProjectDuplicate { 8 | projectId = 0 9 | duplicatedProject: IProject | null = null 10 | parentProjectId = 0 11 | 12 | constructor(data : Partial) { 13 | super() 14 | this.assignData(data) 15 | 16 | this.duplicatedProject = this.duplicatedProject ? new ProjectModel(this.duplicatedProject) : null 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/models/reaction.ts: -------------------------------------------------------------------------------- 1 | import type {IReaction} from '@/modelTypes/IReaction' 2 | import AbstractModel from '@/models/abstractModel' 3 | 4 | export default class ReactionModel extends AbstractModel implements IReaction { 5 | id: number = 0 6 | kind: 'tasks' | 'comments' = 'tasks' 7 | value: string = '' 8 | 9 | constructor(data: Partial) { 10 | super() 11 | this.assignData(data) 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /frontend/src/models/subscription.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | import UserModel from '@/models/user' 3 | 4 | import type {ISubscription} from '@/modelTypes/ISubscription' 5 | import type {IUser} from '@/modelTypes/IUser' 6 | 7 | export default class SubscriptionModel extends AbstractModel implements ISubscription { 8 | id = 0 9 | entity = '' 10 | entityId = 0 11 | user: IUser = {} 12 | 13 | created: Date = null 14 | 15 | constructor(data : Partial) { 16 | super() 17 | this.assignData(data) 18 | 19 | this.created = new Date(this.created) 20 | this.user = new UserModel(this.user) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/models/taskAssignee.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | 3 | import type {ITaskAssignee} from '@/modelTypes/ITaskAssignee' 4 | import type {IUser} from '@/modelTypes/IUser' 5 | import type {ITask} from '@/modelTypes/ITask' 6 | 7 | export default class TaskAssigneeModel extends AbstractModel implements ITaskAssignee { 8 | created: Date = null 9 | userId: IUser['id'] = 0 10 | taskId: ITask['id'] = 0 11 | 12 | constructor(data: Partial) { 13 | super() 14 | this.assignData(data) 15 | this.created = new Date(this.created) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/models/taskBucket.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from '@/models/abstractModel' 2 | import type {ITaskBucket} from '@/modelTypes/ITaskBucket' 3 | import TaskModel from '@/models/task.ts' 4 | import BucketModel from '@/models/bucket.ts' 5 | 6 | export default class TaskBucketModel extends AbstractModel implements ITaskBucket { 7 | taskId = 0 8 | bucketId = 0 9 | projectViewId = 0 10 | projectId = 0 11 | task = undefined 12 | bucket = undefined 13 | 14 | constructor(data: Partial) { 15 | super() 16 | this.assignData(data) 17 | 18 | if (data.task) { 19 | this.task = new TaskModel(data.task) 20 | } 21 | 22 | if (data.bucket) { 23 | this.bucket = new BucketModel(data.bucket) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/models/taskComment.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | import UserModel from './user' 3 | 4 | import type {ITaskComment} from '@/modelTypes/ITaskComment' 5 | import type {ITask} from '@/modelTypes/ITask' 6 | import type {IUser} from '@/modelTypes/IUser' 7 | 8 | export default class TaskCommentModel extends AbstractModel implements ITaskComment { 9 | id = 0 10 | taskId: ITask['id'] = 0 11 | comment = '' 12 | author: IUser = UserModel 13 | 14 | reactions = {} 15 | 16 | created: Date = null 17 | updated: Date = null 18 | 19 | constructor(data: Partial = {}) { 20 | super() 21 | this.assignData(data) 22 | 23 | this.author = new UserModel(this.author) 24 | this.created = new Date(this.created) 25 | this.updated = new Date(this.updated) 26 | 27 | // We can't convert emojis to camel case, hence we do this manually 28 | this.reactions = {} 29 | Object.keys(data.reactions || {}).forEach(reaction => { 30 | this.reactions[reaction] = data.reactions[reaction].map(u => new UserModel(u)) 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/models/taskPosition.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from '@/models/abstractModel' 2 | import type {ITaskPosition} from '@/modelTypes/ITaskPosition' 3 | 4 | export default class TaskPositionModel extends AbstractModel implements ITaskPosition { 5 | position = 0 6 | projectViewId = 0 7 | taskId = 0 8 | 9 | constructor(data: Partial) { 10 | super() 11 | this.assignData(data) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/models/taskRelation.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | import UserModel from './user' 3 | 4 | import type {ITaskRelation} from '@/modelTypes/ITaskRelation' 5 | import type {ITask} from '@/modelTypes/ITask' 6 | import type {IUser} from '@/modelTypes/IUser' 7 | 8 | import {RELATION_KIND, type IRelationKind} from '@/types/IRelationKind' 9 | export default class TaskRelationModel extends AbstractModel implements ITaskRelation { 10 | id = 0 11 | otherTaskId: ITask['id'] = 0 12 | taskId: ITask['id'] = 0 13 | relationKind: IRelationKind = RELATION_KIND.RELATED 14 | 15 | createdBy: IUser = new UserModel() 16 | created: Date = new Date 17 | 18 | constructor(data: Partial) { 19 | super() 20 | this.assignData(data) 21 | 22 | this.createdBy = new UserModel(this.createdBy) 23 | this.created = new Date(this.created) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/models/taskReminder.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | import type {ITaskReminder} from '@/modelTypes/ITaskReminder' 3 | import {parseDateOrNull} from '@/helpers/parseDateOrNull' 4 | import type {IReminderPeriodRelativeTo} from '@/types/IReminderPeriodRelativeTo' 5 | 6 | export default class TaskReminderModel extends AbstractModel implements ITaskReminder { 7 | reminder: Date | null 8 | relativePeriod = 0 9 | relativeTo: IReminderPeriodRelativeTo | null = null 10 | 11 | constructor(data: Partial = {}) { 12 | super() 13 | this.assignData(data) 14 | this.reminder = parseDateOrNull(data.reminder) 15 | if (this.relativeTo === '') { 16 | this.relativeTo = null 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/models/teamMember.ts: -------------------------------------------------------------------------------- 1 | import UserModel from './user' 2 | 3 | import type {ITeamMember} from '@/modelTypes/ITeamMember' 4 | import type {IProject} from '@/modelTypes/IProject' 5 | 6 | export default class TeamMemberModel extends UserModel implements ITeamMember { 7 | admin = false 8 | teamId: IProject['id'] = 0 9 | 10 | constructor(data: Partial) { 11 | super(data) 12 | this.assignData(data) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/models/teamProject.ts: -------------------------------------------------------------------------------- 1 | import TeamShareBaseModel from './teamShareBase' 2 | 3 | import type {ITeamProject} from '@/modelTypes/ITeamProject' 4 | import type {IProject} from '@/modelTypes/IProject' 5 | 6 | export default class TeamProjectModel extends TeamShareBaseModel implements ITeamProject { 7 | projectId: IProject['id'] = 0 8 | 9 | constructor(data: Partial) { 10 | super(data) 11 | this.assignData(data) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/models/teamShareBase.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | 3 | import {RIGHTS, type Right} from '@/constants/rights' 4 | import type {ITeamShareBase} from '@/modelTypes/ITeamShareBase' 5 | import type {ITeam} from '@/modelTypes/ITeam' 6 | 7 | /** 8 | * This class is a base class for common team sharing model. 9 | * It is extended in a way, so it can be used for projects. 10 | */ 11 | export default class TeamShareBaseModel extends AbstractModel implements ITeamShareBase { 12 | teamId: ITeam['id'] = 0 13 | right: Right = RIGHTS.READ 14 | 15 | created: Date = null 16 | updated: Date = null 17 | 18 | constructor(data: Partial) { 19 | super() 20 | this.assignData(data) 21 | 22 | this.created = new Date(this.created) 23 | this.updated = new Date(this.updated) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/models/totp.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | 3 | import type {ITotp} from '@/modelTypes/ITotp' 4 | 5 | export default class TotpModel extends AbstractModel implements ITotp { 6 | secret = '' 7 | enabled = false 8 | url = '' 9 | 10 | constructor(data: Partial = {}) { 11 | super() 12 | this.assignData(data) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/models/userProject.ts: -------------------------------------------------------------------------------- 1 | import UserShareBaseModel from './userShareBase' 2 | 3 | import type {IUserProject} from '@/modelTypes/IUserProject' 4 | import type {IProject} from '@/modelTypes/IProject' 5 | 6 | // This class extends the user share model with a 'rights' parameter which is used in sharing 7 | export default class UserProjectModel extends UserShareBaseModel implements IUserProject { 8 | projectId: IProject['id'] = 0 9 | 10 | constructor(data: Partial) { 11 | super(data) 12 | this.assignData(data) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/models/userShareBase.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from './abstractModel' 2 | 3 | import {RIGHTS, type Right} from '@/constants/rights' 4 | import type {IUserShareBase} from '@/modelTypes/IUserShareBase' 5 | import type {IUser} from '@/modelTypes/IUser' 6 | 7 | export default class UserShareBaseModel extends AbstractModel implements IUserShareBase { 8 | userId: IUser['id'] = '' 9 | right: Right = RIGHTS.READ 10 | 11 | created: Date = null 12 | updated: Date = null 13 | 14 | constructor(data: Partial) { 15 | super() 16 | this.assignData(data) 17 | 18 | this.created = new Date(this.created) 19 | this.updated = new Date(this.updated) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/models/webhook.ts: -------------------------------------------------------------------------------- 1 | import AbstractModel from '@/models/abstractModel' 2 | import type {IWebhook} from '@/modelTypes/IWebhook' 3 | import UserModel from '@/models/user' 4 | 5 | export default class WebhookModel extends AbstractModel implements IWebhook { 6 | id = 0 7 | projectId = 0 8 | secret = '' 9 | targetUrl = '' 10 | events = [] 11 | createdBy = null 12 | 13 | created: Date 14 | updated: Date 15 | 16 | constructor(data: Partial = {}) { 17 | super() 18 | this.assignData(data) 19 | 20 | this.createdBy = new UserModel(this.createdBy) 21 | 22 | this.created = new Date(this.created) 23 | this.updated = new Date(this.updated) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/pinia.ts: -------------------------------------------------------------------------------- 1 | import {createPinia} from 'pinia' 2 | 3 | const pinia = createPinia() 4 | 5 | export default pinia 6 | -------------------------------------------------------------------------------- /frontend/src/registerServiceWorker.ts: -------------------------------------------------------------------------------- 1 | import {register} from 'register-service-worker' 2 | 3 | import {getFullBaseUrl} from './helpers/getFullBaseUrl' 4 | 5 | if (import.meta.env.PROD) { 6 | register(getFullBaseUrl() + 'sw.js', { 7 | ready() { 8 | console.log('App is being served from cache by a service worker.') 9 | }, 10 | registered() { 11 | console.log('Service worker has been registered.') 12 | }, 13 | cached() { 14 | console.log('Content has been cached for offline use.') 15 | }, 16 | updatefound() { 17 | console.log('New content is downloading.') 18 | }, 19 | updated(registration) { 20 | console.log('New content is available; please refresh.') 21 | // Send an event with the updated info 22 | document.dispatchEvent( 23 | new CustomEvent('swUpdated', {detail: registration}), 24 | ) 25 | }, 26 | offline() { 27 | console.log('No internet connection found. App is running in offline mode.') 28 | }, 29 | error(error) { 30 | console.error('Error during service worker registration:', error) 31 | }, 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/services/accountDelete.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | 3 | export default class AccountDeleteService extends AbstractService { 4 | request(password: string) { 5 | return this.post('/user/deletion/request', {password}) 6 | } 7 | 8 | confirm(token: string) { 9 | return this.post('/user/deletion/confirm', {token}) 10 | } 11 | 12 | cancel(password: string) { 13 | return this.post('/user/deletion/cancel', {password}) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/services/apiToken.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from '@/services/abstractService' 2 | import type {IApiToken} from '@/modelTypes/IApiToken' 3 | import ApiTokenModel from '@/models/apiTokenModel' 4 | 5 | export default class ApiTokenService extends AbstractService { 6 | constructor() { 7 | super({ 8 | create: '/tokens', 9 | getAll: '/tokens', 10 | delete: '/tokens/{id}', 11 | }) 12 | } 13 | 14 | processModel(model: IApiToken) { 15 | return { 16 | ...model, 17 | expiresAt: new Date(model.expiresAt).toISOString(), 18 | created: new Date(model.created).toISOString(), 19 | } 20 | } 21 | 22 | modelFactory(data: Partial) { 23 | return new ApiTokenModel(data) 24 | } 25 | 26 | async getAvailableRoutes() { 27 | const cancel = this.setLoading() 28 | 29 | try { 30 | const response = await this.http.get('/routes') 31 | return response.data 32 | } finally { 33 | cancel() 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/services/avatar.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import AvatarModel from '@/models/avatar' 3 | import type { IAvatar } from '@/modelTypes/IAvatar' 4 | 5 | export default class AvatarService extends AbstractService { 6 | constructor() { 7 | super({ 8 | get: '/user/settings/avatar', 9 | update: '/user/settings/avatar', 10 | create: '/user/settings/avatar/upload', 11 | }) 12 | } 13 | 14 | modelFactory(data: Partial) { 15 | return new AvatarModel(data) 16 | } 17 | 18 | useCreateInterceptor() { 19 | return false 20 | } 21 | 22 | create(blob) { 23 | return this.uploadBlob( 24 | this.paths.create, 25 | blob, 26 | 'avatar', 27 | 'avatar.jpg', // This fails without a file name 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/services/backgroundUnsplash.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import BackgroundImageModel from '../models/backgroundImage' 3 | import ProjectModel from '@/models/project' 4 | import type { IBackgroundImage } from '@/modelTypes/IBackgroundImage' 5 | 6 | export default class BackgroundUnsplashService extends AbstractService { 7 | constructor() { 8 | super({ 9 | getAll: '/backgrounds/unsplash/search', 10 | update: '/projects/{projectId}/backgrounds/unsplash', 11 | }) 12 | } 13 | 14 | modelFactory(data: Partial) { 15 | return new BackgroundImageModel(data) 16 | } 17 | 18 | modelUpdateFactory(data) { 19 | return new ProjectModel(data) 20 | } 21 | 22 | async thumb(model) { 23 | const response = await this.http({ 24 | url: `/backgrounds/unsplash/images/${model.id}/thumb`, 25 | method: 'GET', 26 | responseType: 'blob', 27 | }) 28 | return window.URL.createObjectURL(new Blob([response.data])) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/services/backgroundUpload.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import ProjectModel from '@/models/project' 3 | 4 | import type { IProject } from '@/modelTypes/IProject' 5 | import type { IFile } from '@/modelTypes/IFile' 6 | 7 | export default class BackgroundUploadService extends AbstractService { 8 | constructor() { 9 | super({ 10 | create: '/projects/{projectId}/backgrounds/upload', 11 | }) 12 | } 13 | 14 | useCreateInterceptor() { 15 | return false 16 | } 17 | 18 | modelCreateFactory(data: Partial) { 19 | return new ProjectModel(data) 20 | } 21 | 22 | /** 23 | * Uploads a file to the server 24 | */ 25 | create(projectId: IProject['id'], file: IFile) { 26 | return this.uploadFile( 27 | this.getReplacedRoute(this.paths.create, {projectId}), 28 | file, 29 | 'background', 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/services/bucket.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import BucketModel from '../models/bucket' 3 | import TaskService from '@/services/task' 4 | import type { IBucket } from '@/modelTypes/IBucket' 5 | 6 | export default class BucketService extends AbstractService { 7 | constructor() { 8 | super({ 9 | getAll: '/projects/{projectId}/views/{projectViewId}/buckets', 10 | create: '/projects/{projectId}/views/{projectViewId}/buckets', 11 | update: '/projects/{projectId}/views/{projectViewId}/buckets/{id}', 12 | delete: '/projects/{projectId}/views/{projectViewId}/buckets/{id}', 13 | }) 14 | } 15 | 16 | modelFactory(data: Partial) { 17 | return new BucketModel(data) 18 | } 19 | 20 | beforeUpdate(model) { 21 | const taskService = new TaskService() 22 | model.tasks = model.tasks?.map(t => taskService.processModel(t)) 23 | return model 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/services/caldavToken.ts: -------------------------------------------------------------------------------- 1 | import CaldavTokenModel from '@/models/caldavToken' 2 | import type {ICaldavToken} from '@/modelTypes/ICaldavToken' 3 | import AbstractService from './abstractService' 4 | 5 | export default class CaldavTokenService extends AbstractService { 6 | constructor() { 7 | super({ 8 | getAll: '/user/settings/token/caldav', 9 | create: '/user/settings/token/caldav', 10 | delete: '/user/settings/token/caldav/{id}', 11 | }) 12 | } 13 | 14 | modelFactory(data) { 15 | return new CaldavTokenModel(data) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/services/dataExport.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import {downloadBlob} from '../helpers/downloadBlob' 3 | 4 | const DOWNLOAD_NAME = 'vikunja-export.zip' 5 | 6 | export default class DataExportService extends AbstractService { 7 | request(password: string) { 8 | return this.post('/user/export/request', {password}) 9 | } 10 | 11 | async download(password: string) { 12 | const clear = this.setLoading() 13 | try { 14 | const url = await this.getBlobUrl('/user/export/download', 'POST', {password}) 15 | downloadBlob(url, DOWNLOAD_NAME) 16 | } finally { 17 | clear() 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/services/emailUpdate.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | 3 | export default class EmailUpdateService extends AbstractService { 4 | constructor() { 5 | super({ 6 | update: '/user/settings/email', 7 | }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/services/label.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import LabelModel from '@/models/label' 3 | import type {ILabel} from '@/modelTypes/ILabel' 4 | import {colorFromHex} from '@/helpers/color/colorFromHex' 5 | 6 | export default class LabelService extends AbstractService { 7 | constructor() { 8 | super({ 9 | create: '/labels', 10 | getAll: '/labels', 11 | get: '/labels/{id}', 12 | update: '/labels/{id}', 13 | delete: '/labels/{id}', 14 | }) 15 | } 16 | 17 | processModel(label) { 18 | label.created = new Date(label.created).toISOString() 19 | label.updated = new Date(label.updated).toISOString() 20 | label.hexColor = colorFromHex(label.hexColor) 21 | return label 22 | } 23 | 24 | modelFactory(data) { 25 | return new LabelModel(data) 26 | } 27 | 28 | beforeUpdate(label) { 29 | return this.processModel(label) 30 | } 31 | 32 | beforeCreate(label) { 33 | return this.processModel(label) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/services/labelTask.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import LabelTask from '@/models/labelTask' 3 | import type {ILabelTask} from '@/modelTypes/ILabelTask' 4 | 5 | export default class LabelTaskService extends AbstractService { 6 | constructor() { 7 | super({ 8 | create: '/tasks/{taskId}/labels', 9 | getAll: '/tasks/{taskId}/labels', 10 | delete: '/tasks/{taskId}/labels/{labelId}', 11 | }) 12 | } 13 | 14 | modelFactory(data) { 15 | return new LabelTask(data) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/services/linkShare.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import LinkShareModel from '@/models/linkShare' 3 | import type {ILinkShare} from '@/modelTypes/ILinkShare' 4 | 5 | export default class LinkShareService extends AbstractService { 6 | constructor() { 7 | super({ 8 | getAll: '/projects/{projectId}/shares', 9 | get: '/projects/{projectId}/shares/{id}', 10 | create: '/projects/{projectId}/shares', 11 | delete: '/projects/{projectId}/shares/{id}', 12 | }) 13 | } 14 | 15 | modelFactory(data) { 16 | return new LinkShareModel(data) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/services/migrator/abstractMigration.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from '../abstractService' 2 | 3 | export type MigrationConfig = { code: string } 4 | 5 | // This service builds on top of the abstract service and basically just hides away method names. 6 | // It enables migration services to be created with minimal overhead and even better method names. 7 | export default class AbstractMigrationService extends AbstractService { 8 | serviceUrlKey = '' 9 | 10 | constructor(serviceUrlKey: string) { 11 | super({ 12 | update: '/migration/' + serviceUrlKey + '/migrate', 13 | }) 14 | this.serviceUrlKey = serviceUrlKey 15 | } 16 | 17 | getAuthUrl() { 18 | return this.getM('/migration/' + this.serviceUrlKey + '/auth') 19 | } 20 | 21 | getStatus() { 22 | return this.getM('/migration/' + this.serviceUrlKey + '/status') 23 | } 24 | 25 | migrate(data: MigrationConfig) { 26 | return this.update(data) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/services/migrator/abstractMigrationFile.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from '../abstractService' 2 | 3 | // This service builds on top of the abstract service and basically just hides away method names. 4 | // It enables migration services to be created with minimal overhead and even better method names. 5 | export default class AbstractMigrationFileService extends AbstractService { 6 | serviceUrlKey = '' 7 | 8 | constructor(serviceUrlKey: string) { 9 | super({ 10 | create: '/migration/' + serviceUrlKey + '/migrate', 11 | }) 12 | this.serviceUrlKey = serviceUrlKey 13 | } 14 | 15 | getStatus() { 16 | return this.getM('/migration/' + this.serviceUrlKey + '/status') 17 | } 18 | 19 | useCreateInterceptor() { 20 | return false 21 | } 22 | 23 | migrate(file: File) { 24 | return this.uploadFile( 25 | this.paths.create, 26 | file, 27 | 'import', 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/services/notification.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from '@/services/abstractService' 2 | import NotificationModel from '@/models/notification' 3 | import type {INotification} from '@/modelTypes/INotification' 4 | 5 | export default class NotificationService extends AbstractService { 6 | constructor() { 7 | super({ 8 | getAll: '/notifications', 9 | update: '/notifications/{id}', 10 | }) 11 | } 12 | 13 | modelFactory(data) { 14 | return new NotificationModel(data) 15 | } 16 | 17 | beforeUpdate(model) { 18 | if (!model) { 19 | return model 20 | } 21 | 22 | model.created = new Date(model.created).toISOString() 23 | model.readAt = new Date(model.readAt).toISOString() 24 | return model 25 | } 26 | 27 | async markAllRead() { 28 | return this.post('/notifications', false) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/services/passwordReset.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import PasswordResetModel from '@/models/passwordReset' 3 | import type {IPasswordReset} from '@/modelTypes/IPasswordReset' 4 | 5 | export default class PasswordResetService extends AbstractService { 6 | 7 | constructor() { 8 | super({}) 9 | this.paths = { 10 | reset: '/user/password/reset', 11 | requestReset: '/user/password/token', 12 | } 13 | } 14 | 15 | modelFactory(data) { 16 | return new PasswordResetModel(data) 17 | } 18 | 19 | async resetPassword(model) { 20 | const cancel = this.setLoading() 21 | try { 22 | const response = await this.http.post(this.paths.reset, model) 23 | return this.modelFactory(response.data) 24 | } finally { 25 | cancel() 26 | } 27 | } 28 | 29 | async requestResetPassword(model) { 30 | const cancel = this.setLoading() 31 | try { 32 | const response = await this.http.post(this.paths.requestReset, model) 33 | return this.modelFactory(response.data) 34 | } finally { 35 | cancel() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/services/passwordUpdateService.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import type {IPasswordUpdate} from '@/modelTypes/IPasswordUpdate' 3 | 4 | export default class PasswordUpdateService extends AbstractService { 5 | constructor() { 6 | super({ 7 | update: '/user/password', 8 | }) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/services/projectDuplicateService.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import projectDuplicateModel from '@/models/projectDuplicateModel' 3 | import type {IProjectDuplicate} from '@/modelTypes/IProjectDuplicate' 4 | 5 | export default class ProjectDuplicateService extends AbstractService { 6 | constructor() { 7 | super({ 8 | create: '/projects/{projectId}/duplicate', 9 | }) 10 | } 11 | 12 | beforeCreate(model) { 13 | 14 | model.project = null 15 | return model 16 | } 17 | 18 | modelFactory(data) { 19 | return new projectDuplicateModel(data) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/services/projectUsers.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import UserModel from '../models/user' 3 | 4 | export default class ProjectUserService extends AbstractService { 5 | constructor() { 6 | super({ 7 | getAll: '/projects/{projectId}/projectusers', 8 | }) 9 | } 10 | 11 | modelFactory(data) { 12 | return new UserModel(data) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/services/projectViews.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from '@/services/abstractService' 2 | import type {IAbstract} from '@/modelTypes/IAbstract' 3 | import ProjectViewModel from '@/models/projectView' 4 | import type {IProjectView} from '@/modelTypes/IProjectView' 5 | 6 | export default class ProjectViewService extends AbstractService { 7 | constructor() { 8 | super({ 9 | get: '/projects/{projectId}/views/{id}', 10 | getAll: '/projects/{projectId}/views', 11 | create: '/projects/{projectId}/views', 12 | update: '/projects/{projectId}/views/{id}', 13 | delete: '/projects/{projectId}/views/{id}', 14 | }) 15 | } 16 | 17 | modelFactory(data: Partial): ProjectViewModel { 18 | return new ProjectViewModel(data) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/services/reactions.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from '@/services/abstractService' 2 | import type {IAbstract} from '@/modelTypes/IAbstract' 3 | import ReactionModel from '@/models/reaction' 4 | import type {IReactionPerEntity} from '@/modelTypes/IReaction' 5 | import UserModel from '@/models/user' 6 | 7 | export default class ReactionService extends AbstractService { 8 | constructor() { 9 | super({ 10 | getAll: '{kind}/{id}/reactions', 11 | create: '{kind}/{id}/reactions', 12 | delete: '{kind}/{id}/reactions/delete', 13 | }) 14 | } 15 | 16 | modelFactory(data: Partial): ReactionModel { 17 | return new ReactionModel(data) 18 | } 19 | 20 | modelGetAllFactory(data: Partial): Partial { 21 | Object.keys(data).forEach(reaction => { 22 | data[reaction] = data[reaction]?.map(u => new UserModel(u)) 23 | }) 24 | 25 | return data 26 | } 27 | 28 | async delete(model: IAbstract) { 29 | const finalUrl = this.getReplacedRoute(this.paths.delete, model) 30 | return super.post(finalUrl, model) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/services/subscription.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from '@/services/abstractService' 2 | import SubscriptionModel from '@/models/subscription' 3 | import type {ISubscription} from '@/modelTypes/ISubscription' 4 | 5 | export default class SubscriptionService extends AbstractService { 6 | constructor() { 7 | super({ 8 | create: '/subscriptions/{entity}/{entityId}', 9 | delete: '/subscriptions/{entity}/{entityId}', 10 | }) 11 | } 12 | 13 | modelFactory(data) { 14 | return new SubscriptionModel(data) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/services/taskAssignee.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import TaskAssigneeModel from '@/models/taskAssignee' 3 | import type {ITaskAssignee} from '@/modelTypes/ITaskAssignee' 4 | 5 | export default class TaskAssigneeService extends AbstractService { 6 | constructor() { 7 | super({ 8 | create: '/tasks/{taskId}/assignees', 9 | delete: '/tasks/{taskId}/assignees/{userId}', 10 | }) 11 | } 12 | 13 | modelFactory(data) { 14 | return new TaskAssigneeModel(data) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/services/taskBucket.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from '@/services/abstractService' 2 | import type {ITaskBucket} from '@/modelTypes/ITaskBucket' 3 | import TaskBucketModel from '@/models/taskBucket' 4 | 5 | export default class TaskBucketService extends AbstractService { 6 | constructor() { 7 | super({ 8 | update: '/projects/{projectId}/views/{projectViewId}/buckets/{bucketId}/tasks', 9 | }) 10 | } 11 | 12 | modelFactory(data: Partial) { 13 | return new TaskBucketModel(data) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/services/taskPosition.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from '@/services/abstractService' 2 | import type {ITaskPosition} from '@/modelTypes/ITaskPosition' 3 | import TaskPositionModel from '@/models/taskPosition' 4 | 5 | export default class TaskPositionService extends AbstractService { 6 | constructor() { 7 | super({ 8 | update: '/tasks/{taskId}/position', 9 | }) 10 | } 11 | 12 | modelFactory(data: Partial) { 13 | return new TaskPositionModel(data) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/services/taskRelation.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import TaskRelationModel from '@/models/taskRelation' 3 | import type {ITaskRelation} from '@/modelTypes/ITaskRelation' 4 | 5 | export default class TaskRelationService extends AbstractService { 6 | constructor() { 7 | super({ 8 | create: '/tasks/{taskId}/relations', 9 | delete: '/tasks/{taskId}/relations/{relationKind}/{otherTaskId}', 10 | }) 11 | } 12 | 13 | modelFactory(data) { 14 | return new TaskRelationModel(data) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/services/team.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import TeamModel from '@/models/team' 3 | import type {ITeam} from '@/modelTypes/ITeam' 4 | 5 | export default class TeamService extends AbstractService { 6 | constructor() { 7 | super({ 8 | create: '/teams', 9 | get: '/teams/{id}', 10 | getAll: '/teams', 11 | update: '/teams/{id}', 12 | delete: '/teams/{id}', 13 | }) 14 | } 15 | 16 | modelFactory(data) { 17 | return new TeamModel(data) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/services/teamMember.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import TeamMemberModel from '@/models/teamMember' 3 | import type {ITeamMember} from '@/modelTypes/ITeamMember' 4 | 5 | export default class TeamMemberService extends AbstractService { 6 | constructor() { 7 | super({ 8 | create: '/teams/{teamId}/members', 9 | delete: '/teams/{teamId}/members/{username}', 10 | update: '/teams/{teamId}/members/{username}/admin', 11 | }) 12 | } 13 | 14 | modelFactory(data) { 15 | return new TeamMemberModel(data) 16 | } 17 | 18 | beforeCreate(model) { 19 | model.userId = model.id // The api wants to get the user id as user_Id 20 | model.admin = model.admin === null ? false : model.admin 21 | return model 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/services/teamProject.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import TeamProjectModel from '@/models/teamProject' 3 | import type {ITeamProject} from '@/modelTypes/ITeamProject' 4 | import TeamModel from '@/models/team' 5 | 6 | export default class TeamProjectService extends AbstractService { 7 | constructor() { 8 | super({ 9 | create: '/projects/{projectId}/teams', 10 | getAll: '/projects/{projectId}/teams', 11 | update: '/projects/{projectId}/teams/{teamId}', 12 | delete: '/projects/{projectId}/teams/{teamId}', 13 | }) 14 | } 15 | 16 | modelFactory(data) { 17 | return new TeamProjectModel(data) 18 | } 19 | 20 | modelGetAllFactory(data) { 21 | return new TeamModel(data) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/services/totp.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import TotpModel from '@/models/totp' 3 | import type {ITotp} from '@/modelTypes/ITotp' 4 | 5 | export default class TotpService extends AbstractService { 6 | urlPrefix = '/user/settings/totp' 7 | 8 | constructor() { 9 | super({}) 10 | 11 | this.paths.get = this.urlPrefix 12 | } 13 | 14 | modelFactory(data) { 15 | return new TotpModel(data) 16 | } 17 | 18 | enroll() { 19 | return this.post(`${this.urlPrefix}/enroll`, {}) 20 | } 21 | 22 | enable(model) { 23 | return this.post(`${this.urlPrefix}/enable`, model) 24 | } 25 | 26 | disable(model) { 27 | return this.post(`${this.urlPrefix}/disable`, model) 28 | } 29 | 30 | async qrcode() { 31 | const response = await this.http({ 32 | url: `${this.urlPrefix}/qrcode`, 33 | method: 'GET', 34 | responseType: 'blob', 35 | }) 36 | return new Blob([response.data]) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/services/user.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import UserModel from '@/models/user' 3 | import type {IUser} from '@/modelTypes/IUser' 4 | 5 | export default class UserService extends AbstractService { 6 | constructor() { 7 | super({ 8 | getAll: '/users', 9 | }) 10 | } 11 | 12 | modelFactory(data) { 13 | return new UserModel(data) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/services/userProject.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from './abstractService' 2 | import UserProjectModel from '@/models/userProject' 3 | import type {IUserProject} from '@/modelTypes/IUserProject' 4 | import UserModel from '@/models/user' 5 | 6 | export default class UserProjectService extends AbstractService { 7 | constructor() { 8 | super({ 9 | create: '/projects/{projectId}/users', 10 | getAll: '/projects/{projectId}/users', 11 | update: '/projects/{projectId}/users/{userId}', 12 | delete: '/projects/{projectId}/users/{userId}', 13 | }) 14 | } 15 | 16 | modelFactory(data) { 17 | return new UserProjectModel(data) 18 | } 19 | 20 | modelGetAllFactory(data) { 21 | return new UserModel(data) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/services/userSettings.ts: -------------------------------------------------------------------------------- 1 | import type {IUserSettings} from '@/modelTypes/IUserSettings' 2 | import AbstractService from './abstractService' 3 | 4 | export default class UserSettingsService extends AbstractService { 5 | constructor() { 6 | super({ 7 | update: '/user/settings/general', 8 | }) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/services/webhook.ts: -------------------------------------------------------------------------------- 1 | import AbstractService from '@/services/abstractService' 2 | import type {IWebhook} from '@/modelTypes/IWebhook' 3 | import WebhookModel from '@/models/webhook' 4 | 5 | export default class WebhookService extends AbstractService { 6 | constructor() { 7 | super({ 8 | getAll: '/projects/{projectId}/webhooks', 9 | create: '/projects/{projectId}/webhooks', 10 | update: '/projects/{projectId}/webhooks/{id}', 11 | delete: '/projects/{projectId}/webhooks/{id}', 12 | }) 13 | } 14 | 15 | modelFactory(data) { 16 | return new WebhookModel(data) 17 | } 18 | 19 | async getAvailableEvents(): Promise { 20 | const cancel = this.setLoading() 21 | 22 | try { 23 | const response = await this.http.get('/webhooks/events') 24 | return response.data 25 | } finally { 26 | cancel() 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/stores/helper.ts: -------------------------------------------------------------------------------- 1 | const LOADING_TIMEOUT = 100 2 | 3 | export function setModuleLoading(loadFunc: (isLoading: boolean) => void) { 4 | const timeout = setTimeout(() => loadFunc(true), LOADING_TIMEOUT) 5 | return () => { 6 | clearTimeout(timeout) 7 | loadFunc(false) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/styles/components/_index.scss: -------------------------------------------------------------------------------- 1 | @import "tooltip"; 2 | @import "labels"; 3 | @import "task"; 4 | @import "tasks"; 5 | -------------------------------------------------------------------------------- /frontend/src/styles/components/labels.scss: -------------------------------------------------------------------------------- 1 | // FIXME: adapt lables.vue so that it can be used for this aswell 2 | .labels-list { 3 | a, a:hover { 4 | text-decoration: none; 5 | } 6 | 7 | .tag { 8 | margin: .5rem 0 .5rem .5rem; 9 | background: var(--grey-200); 10 | 11 | // FIXME: only used in ListLabels.vue 12 | &.disabled { 13 | opacity: 0.7; 14 | 15 | &, a { 16 | cursor: default; 17 | } 18 | } 19 | } 20 | 21 | .multiselect .tag { 22 | margin: 0 0 0 .5rem; 23 | } 24 | } 25 | 26 | .tasks .task span.tag span { 27 | width: auto; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /frontend/src/styles/components/task.scss: -------------------------------------------------------------------------------- 1 | // FIXME: should be in TaskDetailView.vue 2 | .link-share-container:not(.has-background) .task-view { 3 | background: transparent; 4 | } -------------------------------------------------------------------------------- /frontend/src/styles/components/tooltip.scss: -------------------------------------------------------------------------------- 1 | .v-popper--theme-tooltip .v-popper__inner { 2 | padding: 5px 10px; 3 | font-size: 0.8rem; 4 | background: var(--grey-900); 5 | color: var(--white); 6 | border-radius: 5px; 7 | } 8 | 9 | .dark .v-popper--theme-tooltip .v-popper__inner { 10 | background: var(--white); 11 | color: var(--grey-900); 12 | } -------------------------------------------------------------------------------- /frontend/src/styles/custom-properties/_index.scss: -------------------------------------------------------------------------------- 1 | @import "colors"; 2 | @import "shadows"; -------------------------------------------------------------------------------- /frontend/src/styles/theme/_index.scss: -------------------------------------------------------------------------------- 1 | @import "scrollbars"; 2 | 3 | @import "theme"; 4 | 5 | @import "background"; 6 | @import "content"; 7 | @import "form"; 8 | @import "link-share"; 9 | @import "loading"; 10 | @import "flatpickr"; 11 | @import 'helpers'; 12 | @import 'navigation'; -------------------------------------------------------------------------------- /frontend/src/styles/theme/content.scss: -------------------------------------------------------------------------------- 1 | .content h3 { 2 | .icon, 3 | .is-small { 4 | font-size: 1rem; 5 | } 6 | } 7 | 8 | .table.has-actions { 9 | border-top: 1px solid var(--grey-100); 10 | border-radius: 4px; 11 | overflow: hidden; 12 | 13 | td { 14 | vertical-align: middle; 15 | } 16 | 17 | td.actions { 18 | text-align: right; 19 | } 20 | } 21 | 22 | .content-widescreen { 23 | margin: 0 auto; 24 | max-width: $widescreen; 25 | } 26 | 27 | .content blockquote { 28 | background-color: var(--grey-200); 29 | border-left: .25rem solid var(--grey-300); 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/styles/theme/helpers.scss: -------------------------------------------------------------------------------- 1 | .d-print-none { 2 | @media print { 3 | display: none !important; 4 | } 5 | } -------------------------------------------------------------------------------- /frontend/src/styles/theme/link-share.scss: -------------------------------------------------------------------------------- 1 | .field.has-addons.no-input-mobile { 2 | .control:first-child { 3 | width: 100%; 4 | 5 | @media screen and (max-width: $tablet) { 6 | display: none; 7 | } 8 | } 9 | 10 | .button { 11 | height: 40px; 12 | 13 | @media screen and (max-width: $tablet) { 14 | border-radius: $radius !important; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /frontend/src/styles/theme/loading.scss: -------------------------------------------------------------------------------- 1 | // FIXME: move to Loading.vue 2 | .loader-container.is-loading { 3 | position: relative; 4 | pointer-events: none; 5 | opacity: 0.5; 6 | 7 | &::after { 8 | @include loader; 9 | position: absolute; 10 | top: calc(50% - 2.5rem); 11 | left: calc(50% - 2.5rem); 12 | width: 5rem; 13 | height: 5rem; 14 | border-width: 0.25rem; 15 | } 16 | 17 | &.is-loading-small { 18 | &::after { 19 | width: 1.5rem; 20 | height: 1.5rem; 21 | top: calc(50% - .75rem); 22 | left: calc(50% - .75rem); 23 | border-width: 2px; 24 | } 25 | } 26 | } 27 | 28 | // FIXME: move to ShowTasks.vue 29 | .spinner.is-loading { 30 | pointer-events: none; 31 | 32 | &::after { 33 | @include loader; 34 | width: 2rem; 35 | height: 2rem; 36 | margin-left: calc(50% - 1rem); 37 | margin-top: 1rem; 38 | border-width: 0.25rem; 39 | } 40 | } -------------------------------------------------------------------------------- /frontend/src/styles/theme/scrollbars.scss: -------------------------------------------------------------------------------- 1 | $scrollbar-height: 8px; 2 | $scrollbar-track-color: var(--grey-200); 3 | $scrollbar-thumb-color: var(--grey-300); 4 | $scrollbar-hover-color: var(--grey-500); 5 | 6 | // Chrome 7 | ::-webkit-scrollbar { 8 | width: $scrollbar-height; 9 | height: $scrollbar-height; 10 | } 11 | 12 | ::-webkit-scrollbar-track { 13 | background: $scrollbar-track-color; 14 | border-radius: .5rem; 15 | } 16 | 17 | ::-webkit-scrollbar-thumb { 18 | border-radius: .5rem; 19 | background: $scrollbar-thumb-color; 20 | transition: all $transition; 21 | 22 | &:hover { 23 | background: $scrollbar-hover-color; 24 | } 25 | } 26 | 27 | // Firefox 28 | * { 29 | scrollbar-color: $scrollbar-thumb-color $scrollbar-track-color; 30 | scrollbar-width: thin; 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/styles/transitions.scss: -------------------------------------------------------------------------------- 1 | .fade-enter-active, 2 | .fade-leave-active { 3 | transition: opacity $transition-duration; 4 | } 5 | 6 | .fade-enter-from, 7 | .fade-leave-to { 8 | opacity: 0; 9 | } 10 | 11 | .width-enter-active, 12 | .width-leave-active { 13 | transition: width $transition-duration; 14 | } 15 | 16 | .width-enter-from, 17 | .width-leave-to { 18 | width: 0; 19 | } -------------------------------------------------------------------------------- /frontend/src/types/DateISO.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a date as a string value in ISO format. 3 | * same format as `new Date().toISOString()` 4 | */ 5 | export type DateISO = T 6 | -------------------------------------------------------------------------------- /frontend/src/types/IProvider.ts: -------------------------------------------------------------------------------- 1 | export interface IProvider { 2 | name: string; 3 | key: string; 4 | authUrl: string; 5 | clientId: string; 6 | logoutUrl: string; 7 | scope: string; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/types/IRelationKind.ts: -------------------------------------------------------------------------------- 1 | export enum RELATION_KIND { 2 | 'SUBTASK' = 'subtask', 3 | 'PARENTTASK' = 'parenttask', 4 | 'RELATED' = 'related', 5 | 'DUPLICATES' = 'duplicates', 6 | 'BLOCKING' = 'blocking', 7 | 'BLOCKED' = 'blocked', 8 | 'PROCEDES' = 'precedes', 9 | 'FOLLOWS' = 'follows', 10 | 'COPIEDFROM' = 'copiedfrom', 11 | 'COPIEDTO' = 'copiedto', 12 | } 13 | 14 | export type IRelationKind = typeof RELATION_KIND[keyof typeof RELATION_KIND] 15 | 16 | export const RELATION_KINDS = [...Object.values(RELATION_KIND)] as const 17 | -------------------------------------------------------------------------------- /frontend/src/types/IReminderPeriodRelativeTo.ts: -------------------------------------------------------------------------------- 1 | export const REMINDER_PERIOD_RELATIVE_TO_TYPES = { 2 | DUEDATE: 'due_date', 3 | STARTDATE: 'start_date', 4 | ENDDATE: 'end_date', 5 | } as const 6 | 7 | export type IReminderPeriodRelativeTo = typeof REMINDER_PERIOD_RELATIVE_TO_TYPES[keyof typeof REMINDER_PERIOD_RELATIVE_TO_TYPES] 8 | 9 | -------------------------------------------------------------------------------- /frontend/src/types/IRepeatAfter.ts: -------------------------------------------------------------------------------- 1 | export const REPEAT_TYPES = { 2 | Seconds: 'seconds', 3 | Minutes: 'minutes', 4 | Hours: 'hours', 5 | Days: 'days', 6 | Weeks: 'weeks', 7 | Months: 'months', 8 | Years: 'years', 9 | } as const 10 | 11 | export type IRepeatType = typeof REPEAT_TYPES[keyof typeof REPEAT_TYPES] 12 | 13 | export interface IRepeatAfter { 14 | type: IRepeatType, 15 | amount: number, 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/types/IRepeatMode.ts: -------------------------------------------------------------------------------- 1 | export const TASK_REPEAT_MODES = { 2 | 'REPEAT_MODE_DEFAULT': 0, 3 | 'REPEAT_MODE_MONTH': 1, 4 | 'REPEAT_MODE_FROM_CURRENT_DATE': 2, 5 | } as const 6 | 7 | export type IRepeatMode = typeof TASK_REPEAT_MODES[keyof typeof TASK_REPEAT_MODES] 8 | -------------------------------------------------------------------------------- /frontend/src/types/PartialWithId.ts: -------------------------------------------------------------------------------- 1 | export type PartialWithId = Pick & Omit, 'id'> 2 | -------------------------------------------------------------------------------- /frontend/src/types/cypress.d.ts: -------------------------------------------------------------------------------- 1 | import { mount } from 'cypress/vue' 2 | 3 | type MountParams = Parameters; 4 | type OptionsParam = MountParams[1]; 5 | 6 | declare global { 7 | namespace Cypress { 8 | interface Chainable { 9 | mount: typeof mount; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/types/global-components.d.ts: -------------------------------------------------------------------------------- 1 | import type { FunctionalComponent } from 'vue' 2 | import type { Notifications } from '@kyvg/vue3-notification' 3 | // import FontAwesomeIcon from '@/components/misc/Icon' 4 | import type { FontAwesomeIcon as FontAwesomeIconFixedTypes } from './vue-fontawesome' 5 | import type XButton from '@/components/input/Button.vue' 6 | import type Modal from '@/components/misc/Modal.vue' 7 | import type Card from '@/components/misc/Card.vue' 8 | 9 | // Here we define globally imported components 10 | // See: https://github.com/vuejs/language-tools/wiki/Global-Component-Types 11 | declare module 'vue' { 12 | export interface GlobalComponents { 13 | Icon: FontAwesomeIconFixedTypes 14 | Notifications: FunctionalComponent 15 | XButton: typeof XButton, 16 | Modal: typeof Modal, 17 | Card: typeof Card, 18 | } 19 | } 20 | 21 | export {} 22 | -------------------------------------------------------------------------------- /frontend/src/urls.ts: -------------------------------------------------------------------------------- 1 | export const POWERED_BY = 'https://vikunja.io/?utm_source=powered_by' 2 | export const CALDAV_DOCS = 'https://vikunja.io/docs/caldav/' 3 | -------------------------------------------------------------------------------- /frontend/src/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "VERSION": "dev" 3 | } -------------------------------------------------------------------------------- /frontend/src/views/404.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /frontend/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 39 | -------------------------------------------------------------------------------- /frontend/src/views/filters/FilterDelete.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 26 | -------------------------------------------------------------------------------- /frontend/src/views/migrate/icons/ticktick.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/views/migrate/icons/todoist.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/views/migrate/icons/trello.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/src/views/migrate/icons/vikunja-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/src/views/migrate/icons/vikunja-file.png -------------------------------------------------------------------------------- /frontend/src/views/migrate/icons/wunderlist.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/frontend/src/views/migrate/icons/wunderlist.jpg -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | corePlugins: { 4 | // TODO: Readd after removing bulma base styles 5 | preflight: false, 6 | }, 7 | prefix: 'tw-', 8 | content: [ 9 | './index.html', 10 | './src/**/*.{vue,js,ts}', 11 | ], 12 | theme: { 13 | extend: {}, 14 | }, 15 | plugins: [], 16 | } 17 | 18 | -------------------------------------------------------------------------------- /frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": [ 4 | "env.d.ts", 5 | "src/**/*.d.ts", 6 | "src/**/*", 7 | "src/**/*.vue", 8 | "src/**/*.json", 9 | "tailwind.config.js" 10 | ], 11 | "exclude": ["src/**/__tests__/*"], 12 | "compilerOptions": { 13 | "composite": true, 14 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 15 | "baseUrl": ".", 16 | "lib": ["ESNext", "DOM", "WebWorker"], 17 | 18 | "importHelpers": true, 19 | "sourceMap": true, 20 | "strictNullChecks": true, 21 | 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | }, 25 | "types": [ 26 | // https://github.com/ikenfin/vite-plugin-sentry#typescript 27 | "vite-plugin-sentry/client" 28 | ], 29 | } 30 | } -------------------------------------------------------------------------------- /frontend/tsconfig.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@tsconfig/node22/tsconfig.json", 4 | "@vue/tsconfig/tsconfig.json" 5 | ], 6 | "include": [ 7 | "env.config.d.ts", 8 | "vite.config.*", 9 | "vitest.config.*", 10 | "cypress.config.*" 11 | ], 12 | "compilerOptions": { 13 | "composite": true, 14 | "noEmit": true, 15 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.config.tsbuildinfo", 16 | 17 | "module": "ESNext", 18 | "moduleResolution": "Bundler", 19 | "types": ["node"] 20 | } 21 | } -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.config.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | }, 10 | { 11 | "path": "./tsconfig.vitest.json" 12 | } 13 | ], 14 | "compilerOptions": { 15 | "module": "NodeNext" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/tsconfig.vitest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "exclude": [], 4 | "compilerOptions": { 5 | "composite": true, 6 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo", 7 | 8 | "lib": [], 9 | "types": ["node"] 10 | } 11 | } -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Vikunja is a to-do list application to facilitate your life. 2 | // Copyright 2018-present Vikunja and contributors. All rights reserved. 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public Licensee as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public Licensee for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public Licensee 15 | // along with this program. If not, see . 16 | 17 | package main 18 | 19 | import "code.vikunja.io/api/pkg/cmd" 20 | 21 | func main() { 22 | cmd.Execute() 23 | } 24 | -------------------------------------------------------------------------------- /nfpm.yaml: -------------------------------------------------------------------------------- 1 | name: "vikunja" 2 | arch: "amd64" 3 | platform: "linux" 4 | version: "" 5 | description: "Vikunja is an open-source todo application, written in Go. It lets you create lists,tasks and share them via teams or directly between users." 6 | maintainer: "Vikunja Maintainers " 7 | homepage: "https://vikunja.io" 8 | section: "default" 9 | priority: "extra" 10 | license: "AGPLv3" 11 | depends: 12 | - systemd 13 | contents: 14 | - src: 15 | dst: /opt/vikunja/vikunja 16 | - src: ./config.yml.sample 17 | dst: /etc/vikunja/config.yml 18 | type: "config" 19 | - src: /opt/vikunja/vikunja 20 | dst: /usr/local/bin/vikunja 21 | type: "symlink" 22 | - src: vikunja.service 23 | dst: /usr/lib/systemd/system/vikunja.service 24 | scripts: 25 | postinstall: ./build/after-install.sh 26 | -------------------------------------------------------------------------------- /pkg/db/fixtures/favorites.yml: -------------------------------------------------------------------------------- 1 | - entity_id: 1 2 | user_id: 1 3 | kind: 1 4 | - entity_id: 15 5 | user_id: 6 # owner 6 | kind: 1 7 | - entity_id: 15 8 | user_id: 1 9 | kind: 1 10 | - entity_id: 34 11 | user_id: 13 # owner 12 | kind: 1 13 | - entity_id: 23 14 | user_id: 12 # owner 15 | kind: 2 16 | - entity_id: 23 17 | user_id: 1 18 | kind: 2 19 | -------------------------------------------------------------------------------- /pkg/db/fixtures/files.yml: -------------------------------------------------------------------------------- 1 | - id: 1 2 | name: test 3 | size: 100 4 | created: 2019-10-13 20:33:11 5 | created_by_id: 1 6 | -------------------------------------------------------------------------------- /pkg/db/fixtures/label_tasks.yml: -------------------------------------------------------------------------------- 1 | - id: 1 2 | task_id: 1 3 | label_id: 4 4 | created: 2018-12-01 15:13:12 5 | - id: 2 6 | task_id: 2 7 | label_id: 4 8 | created: 2018-12-01 15:13:12 9 | - id: 3 10 | task_id: 35 11 | label_id: 4 12 | created: 2018-12-01 15:13:12 13 | - id: 4 14 | task_id: 36 15 | label_id: 4 16 | created: 2018-12-01 15:13:12 17 | - id: 5 18 | task_id: 40 19 | label_id: 4 20 | created: 2018-12-01 15:13:12 21 | - id: 6 22 | task_id: 35 23 | label_id: 5 24 | created: 2018-12-01 15:13:12 25 | -------------------------------------------------------------------------------- /pkg/db/fixtures/labels.yml: -------------------------------------------------------------------------------- 1 | - id: 1 2 | title: 'Label #1' 3 | created_by_id: 1 4 | updated: 2018-12-02 15:13:12 5 | created: 2018-12-01 15:13:12 6 | - id: 2 7 | title: 'Label #2' 8 | created_by_id: 1 9 | updated: 2018-12-02 15:13:12 10 | created: 2018-12-01 15:13:12 11 | - id: 3 12 | title: 'Label #3 - other user' 13 | created_by_id: 2 14 | updated: 2018-12-02 15:13:12 15 | created: 2018-12-01 15:13:12 16 | - id: 4 17 | title: 'Label #4 - visible via other task' 18 | created_by_id: 2 19 | updated: 2018-12-02 15:13:12 20 | created: 2018-12-01 15:13:12 21 | - id: 5 22 | title: 'Label #5' 23 | created_by_id: 2 24 | updated: 2018-12-02 15:13:12 25 | created: 2018-12-01 15:13:12 26 | -------------------------------------------------------------------------------- /pkg/db/fixtures/link_shares.yml: -------------------------------------------------------------------------------- 1 | - id: 1 2 | hash: test 3 | project_id: 1 4 | right: 0 5 | sharing_type: 1 6 | shared_by_id: 1 7 | created: 2018-12-01 15:13:12 8 | updated: 2018-12-02 15:13:12 9 | - id: 2 10 | hash: test2 11 | project_id: 2 12 | right: 1 13 | sharing_type: 1 14 | shared_by_id: 1 15 | created: 2018-12-01 15:13:12 16 | updated: 2018-12-02 15:13:12 17 | - id: 3 18 | hash: test3 19 | project_id: 3 20 | right: 2 21 | sharing_type: 1 22 | shared_by_id: 1 23 | created: 2018-12-01 15:13:12 24 | updated: 2018-12-02 15:13:12 25 | - id: 4 26 | hash: testWithPassword 27 | project_id: 1 28 | right: 0 29 | password: '$2a$04$X4aRMEt0ytgPwMIgv36cI..7X9.nhY/.tYwxpqSi0ykRHx2CwQ0S6' # 12345678 30 | sharing_type: 2 31 | shared_by_id: 1 32 | created: 2018-12-01 15:13:12 33 | updated: 2018-12-02 15:13:12 34 | -------------------------------------------------------------------------------- /pkg/db/fixtures/reactions.yml: -------------------------------------------------------------------------------- 1 | - id: 1 2 | user_id: 1 3 | entity_id: 1 4 | entity_kind: 0 5 | value: '👋' 6 | created: 2024-03-12 15:00:00 7 | -------------------------------------------------------------------------------- /pkg/db/fixtures/saved_filters.yml: -------------------------------------------------------------------------------- 1 | - id: 1 2 | filters: '{"sort_by":null,"order_by":null,"filter":"start_date > \u00272018-12-11T03:46:40+00:00\u0027 || end_date < \u00272018-12-13T11:20:01+00:00\u0027 || due_date > \u00272018-11-29T14:00:00+00:00\u0027","filter_include_nulls":false}' 3 | title: testfilter1 4 | owner_id: 1 5 | updated: 2020-09-08 15:13:12 6 | created: 2020-09-08 14:13:12 7 | -------------------------------------------------------------------------------- /pkg/db/fixtures/subscriptions.yml: -------------------------------------------------------------------------------- 1 | - id: 1 2 | entity_type: 3 # Task 3 | entity_id: 2 4 | user_id: 1 5 | created: 2021-02-01 15:13:12 6 | - id: 3 7 | entity_type: 2 # project 8 | entity_id: 12 # belongs to parent project 7 9 | user_id: 6 10 | created: 2021-02-01 15:13:12 11 | - id: 4 12 | entity_type: 3 # Task 13 | entity_id: 22 # belongs to project 13 14 | user_id: 6 15 | created: 2021-02-01 15:13:12 16 | - id: 6 17 | entity_type: 2 # project 18 | entity_id: 13 19 | user_id: 6 20 | created: 2021-02-01 15:13:12 21 | - id: 7 22 | entity_type: 3 # Task 23 | entity_id: 26 24 | user_id: 6 25 | created: 2021-02-01 15:13:12 26 | - id: 8 27 | entity_type: 2 # Project 28 | entity_id: 32 29 | user_id: 6 30 | created: 2021-02-01 15:13:12 31 | - id: 9 32 | entity_type: 3 # Task 33 | entity_id: 18 34 | user_id: 6 35 | created: 2021-02-01 15:13:12 36 | - id: 10 37 | entity_type: 2 # Project 38 | entity_id: 9 39 | user_id: 6 40 | created: 2021-02-01 15:13:12 41 | -------------------------------------------------------------------------------- /pkg/db/fixtures/task_assignees.yml: -------------------------------------------------------------------------------- 1 | - id: 1 2 | task_id: 30 3 | user_id: 1 4 | created: 2018-12-01 15:13:12 5 | - id: 2 6 | task_id: 30 7 | user_id: 2 8 | created: 2018-12-01 15:13:12 9 | - id: 3 10 | task_id: 35 11 | user_id: 2 12 | created: 2018-12-01 15:13:12 13 | - id: 4 14 | task_id: 36 15 | user_id: 2 16 | created: 2018-12-01 15:13:12 17 | -------------------------------------------------------------------------------- /pkg/db/fixtures/task_attachments.yml: -------------------------------------------------------------------------------- 1 | - id: 1 2 | task_id: 1 3 | file_id: 1 4 | created_by_id: 1 5 | created: 2018-12-01 15:13:12 6 | # The file for this attachment does not exist 7 | - id: 2 8 | task_id: 1 9 | file_id: 9999 10 | created_by_id: 1 11 | created: 2018-12-01 15:13:12 12 | - id: 3 13 | task_id: 1 14 | file_id: 1 15 | created_by_id: -2 16 | created: 2018-12-01 15:13:12 17 | -------------------------------------------------------------------------------- /pkg/db/fixtures/task_reminders.yml: -------------------------------------------------------------------------------- 1 | - id: 1 2 | task_id: 27 3 | reminder: 2018-12-01 01:12:04 4 | created: 2018-12-01 01:12:04 5 | - id: 2 6 | task_id: 27 7 | reminder: 2018-12-01 01:13:44 8 | created: 2018-12-01 01:12:04 9 | relative_to: 'start_date' 10 | relative_period: -3600 11 | - id: 3 12 | task_id: 2 13 | reminder: 2018-12-01 01:13:44 14 | created: 2018-12-01 01:12:04 15 | - id: 4 16 | task_id: 40 17 | reminder: 2023-03-04 15:00:00 18 | created: 2018-12-01 01:12:04 19 | -------------------------------------------------------------------------------- /pkg/db/fixtures/teams.yml: -------------------------------------------------------------------------------- 1 | - id: 1 2 | name: testteam1 3 | description: Lorem Ipsum 4 | created_by_id: 1 5 | - id: 2 6 | name: testteam2_read_only_on_project6 7 | created_by_id: 1 8 | - id: 3 9 | name: testteam3_write_on_project7 10 | created_by_id: 1 11 | - id: 4 12 | name: testteam4_admin_on_project8 13 | created_by_id: 1 14 | - id: 8 15 | name: testteam8 16 | created_by_id: 7 17 | - id: 9 18 | name: testteam9 19 | created_by_id: 7 20 | - id: 10 21 | name: testteam10 22 | created_by_id: 7 23 | - id: 11 24 | name: testteam11 25 | created_by_id: 7 26 | - id: 12 27 | name: testteam12 28 | created_by_id: 7 29 | - id: 13 30 | name: testteam13 31 | created_by_id: 7 32 | is_public: true 33 | - id: 14 34 | name: testteam14 35 | created_by_id: 7 36 | external_id: 14 37 | issuer: "https://some.issuer" 38 | - id: 15 39 | name: testteam15 40 | created_by_id: 7 41 | external_id: 15 42 | issuer: "https://some.issuer" 43 | is_public: true 44 | description: "This is a public team" 45 | -------------------------------------------------------------------------------- /pkg/db/fixtures/user_tokens.yml: -------------------------------------------------------------------------------- 1 | - 2 | id: 1 3 | user_id: 3 4 | token: 'passwordresettesttoken' 5 | kind: 1 6 | created: 2021-07-12 00:00:11 7 | - 8 | id: 2 9 | user_id: 4 10 | token: 'tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael' 11 | kind: 2 12 | created: 2021-07-12 00:00:12 13 | - 14 | id: 3 15 | user_id: 5 16 | token: 'tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Aei' 17 | kind: 2 18 | created: 2021-07-12 00:00:13 19 | -------------------------------------------------------------------------------- /pkg/i18n/lang/ar-SA.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/bg-BG.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/ca-ES.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/cs-CZ.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/da-DK.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/eo-UY.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/es-ES.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/hr-HR.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/hu-HU.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/it-IT.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/ja-JP.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/lt-LT.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/nl-NL.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/pl-PL.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/pt-BR.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/ro-RO.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/sk-SK.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/sl-SI.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/sr-CS.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/sv-SE.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/uk-UA.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/zh-CN.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/i18n/lang/zh-TW.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pkg/models/message.go: -------------------------------------------------------------------------------- 1 | // Vikunja is a to-do list application to facilitate your life. 2 | // Copyright 2018-present Vikunja and contributors. All rights reserved. 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public Licensee as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public Licensee for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public Licensee 15 | // along with this program. If not, see . 16 | 17 | package models 18 | 19 | // Message is a standard message 20 | type Message struct { 21 | // A standard message. 22 | Message string `json:"message"` 23 | } 24 | -------------------------------------------------------------------------------- /pkg/modules/migration/db.go: -------------------------------------------------------------------------------- 1 | // Vikunja is a to-do list application to facilitate your life. 2 | // Copyright 2018-present Vikunja and contributors. All rights reserved. 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public Licensee as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public Licensee for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public Licensee 15 | // along with this program. If not, see . 16 | 17 | package migration 18 | 19 | // GetTables returns all structs which are also a table. 20 | func GetTables() []interface{} { 21 | return []interface{}{ 22 | &Status{}, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/modules/migration/testimage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/pkg/modules/migration/testimage.jpg -------------------------------------------------------------------------------- /pkg/modules/migration/vikunja-file/export.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/pkg/modules/migration/vikunja-file/export.zip -------------------------------------------------------------------------------- /pkg/modules/migration/vikunja-file/export_pre_0.21.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/pkg/modules/migration/vikunja-file/export_pre_0.21.0.zip -------------------------------------------------------------------------------- /pkg/notifications/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-vikunja/vikunja/85fe915cbcf96109f6469d944e0ea7e8aa47ee7e/pkg/notifications/logo.png -------------------------------------------------------------------------------- /pkg/utils/umask_unix.go: -------------------------------------------------------------------------------- 1 | // Vikunja is a to-do list application to facilitate your life. 2 | // Copyright 2018-present Vikunja and contributors. All rights reserved. 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public Licensee as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public Licensee for more details. 13 | // 14 | // You should have received a copy of the GNU Affero General Public Licensee 15 | // along with this program. If not, see . 16 | 17 | //go:build !windows 18 | // +build !windows 19 | 20 | package utils 21 | 22 | import "golang.org/x/sys/unix" 23 | 24 | func Umask(mask int) int { 25 | return unix.Umask(mask) 26 | } 27 | -------------------------------------------------------------------------------- /rest/bruno.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1", 3 | "name": "API-Requests", 4 | "type": "collection" 5 | } -------------------------------------------------------------------------------- /rest/environments/local.bru: -------------------------------------------------------------------------------- 1 | vars { 2 | host: http://localhost:3456 3 | } 4 | -------------------------------------------------------------------------------- /rest/login.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: login 3 | type: http 4 | seq: 1 5 | } 6 | 7 | post { 8 | url: {{host}}/api/v1/login 9 | body: json 10 | } 11 | 12 | body:json { 13 | { 14 | "username": "{{username}}", 15 | "password": "{{password}}" 16 | } 17 | } 18 | 19 | vars:pre-request { 20 | username: test 21 | password: 12345678 22 | } 23 | 24 | vars:post-response { 25 | token: res.body.token 26 | } 27 | -------------------------------------------------------------------------------- /rest/mark all notifications as read.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: mark all notifications as read 3 | type: http 4 | seq: 3 5 | } 6 | 7 | post { 8 | url: {{host}}/api/v1/notifications 9 | body: none 10 | } 11 | 12 | headers { 13 | Authorization: Bearer {{token}} 14 | } 15 | -------------------------------------------------------------------------------- /rest/user info.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: user info 3 | type: http 4 | seq: 2 5 | } 6 | 7 | get { 8 | url: {{host}}/api/v1/user 9 | body: none 10 | } 11 | 12 | headers { 13 | Authorization: Bearer {{token}} 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "project": "./frontend/tsconfig.json", 4 | }, 5 | } -------------------------------------------------------------------------------- /vikunja.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Vikunja 3 | After=syslog.target 4 | After=network.target 5 | # Depending on how you configured Vikunja, you may want to uncomment these: 6 | #Requires=mysql.service 7 | #Requires=mariadb.service 8 | #Requires=postgresql.service 9 | #Requires=redis.service 10 | 11 | [Service] 12 | RestartSec=2s 13 | Type=simple 14 | WorkingDirectory=/opt/vikunja 15 | ExecStart=/usr/local/bin/vikunja 16 | Restart=always 17 | # If you want to bind Vikunja to a port below 1024 uncomment 18 | # the two values below 19 | ### 20 | #CapabilityBoundingSet=CAP_NET_BIND_SERVICE 21 | #AmbientCapabilities=CAP_NET_BIND_SERVICE 22 | 23 | [Install] 24 | WantedBy=multi-user.target --------------------------------------------------------------------------------