├── .drone.yml
├── .editorconfig
├── .env.local.example
├── .envrc
├── .eslintrc.cjs
├── .gitea
└── issue_template.md
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug-report.yml
│ └── config.yml
└── workflows
│ └── lockdown.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .vscode.example
├── extensions.json
└── settings.json
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── README.md
├── cliff.toml
├── cypress.config.js
├── 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
│ │ ├── registration.spec.ts
│ │ └── settings.spec.ts
├── factories
│ ├── bucket.ts
│ ├── label_task.ts
│ ├── labels.ts
│ ├── link_sharing.ts
│ ├── project.ts
│ ├── task.ts
│ ├── task_assignee.ts
│ ├── task_attachments.ts
│ ├── task_comment.ts
│ ├── task_reminders.ts
│ ├── team.ts
│ ├── team_member.ts
│ ├── user.ts
│ └── users_project.ts
├── fixtures
│ └── image.jpg
└── support
│ ├── authenticateUser.ts
│ ├── commands.ts
│ ├── component.index.html
│ ├── component.ts
│ ├── e2e.ts
│ ├── factory.ts
│ ├── seed.ts
│ └── updateUserSettings.ts
├── docker
├── injector.sh
├── ipv6-disable.sh
├── nginx.conf
└── templates
│ └── default.conf.template
├── docs
└── models-services.md
├── env.config.d.ts
├── env.d.ts
├── flake.lock
├── flake.nix
├── 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
└── flexsearch@0.7.31.patch
├── pnpm-lock.yaml
├── public
├── 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
├── renovate.json
├── 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
│ │ └── Expandable.vue
│ ├── date
│ │ ├── dateRanges.ts
│ │ ├── datemathHelp.story.vue
│ │ ├── datemathHelp.vue
│ │ └── datepickerWithRange.vue
│ ├── home
│ │ ├── AddToHomeScreen.vue
│ │ ├── DemoMode.vue
│ │ ├── Logo.vue
│ │ ├── MenuButton.vue
│ │ ├── PoweredByLink.vue
│ │ ├── ProjectsNavigation.vue
│ │ ├── ProjectsNavigationItem.vue
│ │ ├── TheNavigation.vue
│ │ ├── UpdateNotification.vue
│ │ ├── contentAuth.vue
│ │ ├── contentLinkShare.vue
│ │ └── navigation.vue
│ ├── input
│ │ ├── AsyncEditor.ts
│ │ ├── Button.story.vue
│ │ ├── ColorPicker.story.vue
│ │ ├── ColorPicker.vue
│ │ ├── SelectProject.vue
│ │ ├── SelectUser.vue
│ │ ├── SimpleButton.vue
│ │ ├── button.vue
│ │ ├── datepicker.vue
│ │ ├── datepickerInline.vue
│ │ ├── editor
│ │ │ ├── CommandsList.vue
│ │ │ ├── EditorToolbar.vue
│ │ │ ├── TipTap.vue
│ │ │ ├── commands.ts
│ │ │ ├── setLinkInEditor.ts
│ │ │ ├── suggestion.ts
│ │ │ └── types.ts
│ │ ├── fancycheckbox.story.vue
│ │ ├── fancycheckbox.vue
│ │ ├── multiselect.vue
│ │ └── password.vue
│ ├── misc
│ │ ├── ButtonLink.vue
│ │ ├── Card.story.vue
│ │ ├── CustomTransition.vue
│ │ ├── Done.vue
│ │ ├── Icon.ts
│ │ ├── OpenQuickActions.vue
│ │ ├── ProgressBar.story.vue
│ │ ├── ProgressBar.vue
│ │ ├── api-config.vue
│ │ ├── card.vue
│ │ ├── colorBubble.vue
│ │ ├── create-edit.vue
│ │ ├── dropdown-item.vue
│ │ ├── dropdown.vue
│ │ ├── error.vue
│ │ ├── flatpickr
│ │ │ └── Flatpickr.vue
│ │ ├── keyboard-shortcuts
│ │ │ ├── index.vue
│ │ │ └── shortcuts.ts
│ │ ├── legal.vue
│ │ ├── loading.vue
│ │ ├── message.vue
│ │ ├── modal.vue
│ │ ├── no-auth-wrapper.vue
│ │ ├── nothing.vue
│ │ ├── notification.vue
│ │ ├── pagination.vue
│ │ ├── popup.vue
│ │ ├── ready.vue
│ │ ├── shortcut.vue
│ │ ├── subscription.vue
│ │ └── user.vue
│ ├── notifications
│ │ └── notifications.vue
│ ├── project
│ │ ├── ProjectWrapper.vue
│ │ ├── partials
│ │ │ ├── ProjectCard.vue
│ │ │ ├── ProjectCardGrid.vue
│ │ │ ├── filter-popup.vue
│ │ │ ├── filters.vue
│ │ │ └── useProjectBackground.ts
│ │ └── project-settings-dropdown.vue
│ ├── quick-actions
│ │ └── quick-actions.vue
│ ├── sharing
│ │ ├── linkSharing.vue
│ │ └── userTeam.vue
│ └── tasks
│ │ ├── GanttChart.vue
│ │ ├── TaskForm.vue
│ │ ├── add-task.vue
│ │ └── partials
│ │ ├── assigneeList.vue
│ │ ├── attachments.vue
│ │ ├── checklist-summary.vue
│ │ ├── comments.vue
│ │ ├── createdUpdated.vue
│ │ ├── date-table-cell.vue
│ │ ├── defer-task.vue
│ │ ├── description.vue
│ │ ├── editAssignees.vue
│ │ ├── editLabels.vue
│ │ ├── heading.vue
│ │ ├── kanban-card.vue
│ │ ├── label.vue
│ │ ├── labels.vue
│ │ ├── percentDoneSelect.vue
│ │ ├── priorityLabel.vue
│ │ ├── prioritySelect.vue
│ │ ├── projectSearch.vue
│ │ ├── quick-add-magic.vue
│ │ ├── relatedTasks.vue
│ │ ├── reminder-detail.vue
│ │ ├── reminder-period.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
│ ├── useMenuActive.ts
│ ├── useOnline.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
│ ├── canNestProjectDeeper.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
│ ├── flatpickrLanguage.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
│ │ ├── parseKebabDate.ts
│ │ └── period.ts
│ └── utils.ts
├── histoire.setup.ts
├── i18n
│ ├── index.ts
│ ├── lang
│ │ ├── ar-SA.json
│ │ ├── ca-ES.json
│ │ ├── cs-CZ.json
│ │ ├── da-DK.json
│ │ ├── de-DE.json
│ │ ├── de-swiss.json
│ │ ├── en.json
│ │ ├── eo-UY.json
│ │ ├── es-ES.json
│ │ ├── fr-FR.json
│ │ ├── hu-HU.json
│ │ ├── it-IT.json
│ │ ├── ja-JP.json
│ │ ├── ko-KR.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
│ │ ├── 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
│ ├── ISavedFilter.ts
│ ├── ISubscription.ts
│ ├── ITask.ts
│ ├── ITaskAssignee.ts
│ ├── ITaskComment.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
│ ├── savedFilter.ts
│ ├── subscription.ts
│ ├── task.ts
│ ├── taskAssignee.ts
│ ├── taskComment.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
├── polyfills.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
│ ├── savedFilter.ts
│ ├── subscription.ts
│ ├── task.ts
│ ├── taskAssignee.ts
│ ├── taskCollection.ts
│ ├── taskComment.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
│ │ ├── project.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
│ ├── DateKebab.ts
│ ├── IFilter.ts
│ ├── IProvider.ts
│ ├── IRelationKind.ts
│ ├── IReminderPeriodRelativeTo.ts
│ ├── IRepeatAfter.ts
│ ├── IRepeatMode.ts
│ ├── PartialWithId.ts
│ ├── ProjectView.ts
│ ├── cypress.d.ts
│ ├── global-components.d.ts
│ ├── shims-tsx.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
│ ├── ProjectGantt.vue
│ ├── ProjectInfo.vue
│ ├── ProjectKanban.vue
│ ├── ProjectList.vue
│ ├── ProjectTable.vue
│ ├── helpers
│ │ ├── useGanttFilters.ts
│ │ └── useGanttTaskList.ts
│ └── settings
│ │ ├── archive.vue
│ │ ├── background.vue
│ │ ├── delete.vue
│ │ ├── duplicate.vue
│ │ ├── edit.vue
│ │ ├── share.vue
│ │ └── webhooks.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
├── tsconfig.app.json
├── tsconfig.config.json
├── tsconfig.json
├── tsconfig.vitest.json
└── vite.config.ts
/.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 = false
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
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/.envrc:
--------------------------------------------------------------------------------
1 | use flake
2 |
--------------------------------------------------------------------------------
/.gitea/issue_template.md:
--------------------------------------------------------------------------------
1 |
7 |
8 | **Version information:**
9 |
10 | Frontend Version:
11 | API Version:
12 | Browser and OS Version:
13 |
14 | **Steps to reproduce:**
15 |
16 |
19 |
20 | 1.
21 | 2.
22 | ...
23 |
24 | **Expected behavior:**
25 |
26 |
29 |
30 |
31 |
32 | **Actual behavior:**
33 |
34 |
37 |
38 |
39 |
40 | **Checklist:**
41 |
42 | * [ ] I have provided all required information
43 | * [ ] I am using the latest release or the latest unstable build
44 | * [ ] I was able to reproduce the bug on [try](https://try.vikunja.io)
45 |
--------------------------------------------------------------------------------
/.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: API issues
4 | url: https://code.vikunja.io/api/issues
5 | about: This is the frontend repo. Please open api-related bug reports and discussions in the api 0repo. Not sure if your issue is frontend or api? Ask in Matrix or the forum first.
6 | - name: Forum
7 | url: https://community.vikunja.io/
8 | about: Feature Requests, Questions, configuration or deployment problems should be discussed in the forum.
9 | - name: Security-related issues
10 | url: https://vikunja.io/contact/#security
11 | about: For security concerns, please send a mail to security@vikunja.io instead of opening a public issue.
12 | - name: Chat on Matrix
13 | url: https://matrix.to/#/#vikunja:matrix.org
14 | about: Please ask any quick questions here.
15 | - name: Translations
16 | url: https://crowdin.com/project/vikunja
17 | about: Any problems or requests for new languages about translations should be handled in crowdin.
18 |
--------------------------------------------------------------------------------
/.github/workflows/lockdown.yml:
--------------------------------------------------------------------------------
1 | name: 'Repo Lockdown'
2 |
3 | on:
4 | pull_request_target:
5 | types: opened
6 |
7 | permissions:
8 | issues: write
9 | pull-requests: write
10 |
11 | jobs:
12 | action:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: dessant/repo-lockdown@v4
16 | with:
17 | pr-comment: 'Hi! Thank you for your contribution.
18 |
19 | This repo is only a mirror and unfortunately we can''t accept PRs made here. Please re-submit your changes to [our Gitea instance](https://kolaente.dev/vikunja/frontend/pulls).
20 |
21 | Also check out the [contribution guidelines](https://vikunja.io/docs/development/#pull-requests).
22 |
23 | Thank you for your understanding.'
--------------------------------------------------------------------------------
/.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 | coverage
15 | *.zip
16 | .direnv/
17 |
18 | # Test files
19 | cypress/screenshots
20 | cypress/videos
21 |
22 | # local env files
23 | .env.local
24 | .env.*.local
25 |
26 | # Editor directories and files
27 | .vscode
28 | .idea
29 | *.suo
30 | *.ntvs*
31 | *.njsproj
32 | *.sln
33 | *.sw*
34 | !rollup.sw.js
35 |
36 |
37 | # Local Netlify folder
38 | .netlify
39 |
40 | # histoire
41 | .histoire
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | fetch-timeout=100000
2 |
3 | # pnpm settings
4 | # The following settings prepare for the new default value of pnpm 8
5 | # they can be removed directly after having moved to pnpm 8
6 | auto-install-peers=true
7 | dedupe-peer-dependents=true
8 | resolve-peers-from-workspace-root=true
9 | save-workspace-protocol=rolling
10 | resolution-mode=lowest-direct
11 | publishConfig.linkDirectory=true
12 |
13 | # remove some time after having moved to pnpm 8
14 | use-lockfile-v6=true
15 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 20.11.0
--------------------------------------------------------------------------------
/.vscode.example/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "codezombiech.gitignore",
4 | "dbaeumer.vscode-eslint",
5 | "editorconfig.editorconfig",
6 | "vue.volar",
7 | "vue.vscode-typescript-vue-plugin",
8 | "lokalise.i18n-ally",
9 | "mgmcdermott.vscode-language-babel",
10 | "mikestead.dotenv",
11 | "Syler.sass-indented",
12 | "zixuanchen.vitest-explorer"
13 | ]
14 | }
--------------------------------------------------------------------------------
/.vscode.example/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.packageManager": "pnpm",
3 | "editor.formatOnSave": false,
4 | "editor.codeActionsOnSave": {
5 | "source.fixAll": true
6 | },
7 | "eslint.format.enable": true,
8 | "[javascript]": {
9 | "editor.defaultFormatter": "dbaeumer.vscode-eslint"
10 | },
11 | "[typescript]": {
12 | "editor.defaultFormatter": "dbaeumer.vscode-eslint"
13 | },
14 |
15 | // https://eslint.vuejs.org/user-guide/#editor-integrations
16 | "eslint.validate": [
17 | "javascript",
18 | "javascriptreact",
19 | "vue"
20 | ],
21 |
22 | "volar.completion.preferredTagNameCase": "pascal",
23 |
24 | // disable vetur in case it is installed
25 | "vetur.validation.template": false,
26 |
27 | // i18n ally
28 | "i18n-ally.localesPaths": [
29 | "src/i18n/lang"
30 | ],
31 | "i18n-ally.sortKeys": true,
32 | "i18n-ally.keepFulfilled": true,
33 | "i18n-ally.keystyle": "nested"
34 | }
--------------------------------------------------------------------------------
/cypress.config.js:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/cypress/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | api:
5 | image: vikunja/api:unstable
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
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/cypress/e2e/project/prepareProjects.ts:
--------------------------------------------------------------------------------
1 | import {ProjectFactory} from '../../factories/project'
2 | import {TaskFactory} from '../../factories/task'
3 |
4 | export function createProjects() {
5 | const projects = ProjectFactory.create(1, {
6 | title: 'First Project'
7 | })
8 | TaskFactory.truncate()
9 | return projects
10 | }
11 |
12 | export function prepareProjects(setProjects = (...args: any[]) => {}) {
13 | beforeEach(() => {
14 | const projects = createProjects()
15 | setProjects(projects)
16 | })
17 | }
--------------------------------------------------------------------------------
/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 | "ignoreDeprecations": "5.0"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/cypress/e2e/user/logout.spec.ts:
--------------------------------------------------------------------------------
1 | import {createFakeUserAndLogin} from '../../support/authenticateUser'
2 | import {createProjects} from '../project/prepareProjects'
3 |
4 | function logout() {
5 | cy.get('.navbar .username-dropdown-trigger')
6 | .click()
7 | cy.get('.navbar .dropdown-item')
8 | .contains('Logout')
9 | .click()
10 | }
11 |
12 | describe('Log out', () => {
13 | createFakeUserAndLogin()
14 |
15 | it('Logs the user out', () => {
16 | cy.visit('/')
17 |
18 | expect(localStorage.getItem('token')).to.not.eq(null)
19 |
20 | logout()
21 |
22 | cy.url()
23 | .should('contain', '/login')
24 | .then(() => {
25 | expect(localStorage.getItem('token')).to.eq(null)
26 | })
27 | })
28 |
29 | it.skip('Should clear the project history after logging the user out', () => {
30 | const projects = createProjects()
31 | cy.visit(`/projects/${projects[0].id}`)
32 | .then(() => {
33 | expect(localStorage.getItem('projectHistory')).to.not.eq(null)
34 | })
35 |
36 | logout()
37 |
38 | cy.wait(1000) // This makes re-loading of the project and associated entities (and the resulting error) visible
39 |
40 | cy.url()
41 | .should('contain', '/login')
42 | .then(() => {
43 | expect(localStorage.getItem('projectHistory')).to.eq(null)
44 | })
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/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_id: 1,
14 | created_by_id: 1,
15 | created: now.toISOString(),
16 | updated: now.toISOString(),
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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.random.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 |
--------------------------------------------------------------------------------
/cypress/factories/project.ts:
--------------------------------------------------------------------------------
1 | import {Factory} from '../support/factory'
2 | import {faker} from '@faker-js/faker'
3 |
4 | export class ProjectFactory extends Factory {
5 | static table = 'projects'
6 |
7 | static factory() {
8 | const now = new Date()
9 |
10 | return {
11 | id: '{increment}',
12 | title: faker.lorem.words(3),
13 | owner_id: 1,
14 | created: now.toISOString(),
15 | updated: now.toISOString(),
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/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 | position: '{increment}',
18 | created: now.toISOString(),
19 | updated: now.toISOString()
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/cypress/factories/user.ts:
--------------------------------------------------------------------------------
1 | import {faker} from '@faker-js/faker'
2 |
3 | import {Factory} from '../support/factory'
4 |
5 | export class UserFactory extends Factory {
6 | static table = 'users'
7 |
8 | static factory() {
9 | const now = new Date()
10 |
11 | return {
12 | id: '{increment}',
13 | username: faker.lorem.word(10) + faker.datatype.uuid(),
14 | password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.', // 1234
15 | status: 0,
16 | issuer: 'local',
17 | created: now.toISOString(),
18 | updated: now.toISOString(),
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/cypress/fixtures/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/cypress/fixtures/image.jpg
--------------------------------------------------------------------------------
/cypress/support/authenticateUser.ts:
--------------------------------------------------------------------------------
1 |
2 | // This authenticates a user and puts the token in local storage which allows us to perform authenticated requests.
3 | // Built after https://github.com/cypress-io/cypress-example-recipes/tree/bd2d6ffb33214884cab343d38e7f9e6ebffb323f/examples/logging-in__jwt
4 |
5 | import {UserFactory} from '../factories/user'
6 |
7 | export function login(user, cacheAcrossSpecs = false) {
8 | if (!user) {
9 | throw new Error('Needs user')
10 | }
11 | // Caching session when logging in via page visit
12 | cy.session(`user__${user.username}`, () => {
13 | cy.request('POST', `${Cypress.env('API_URL')}/login`, {
14 | username: user.username,
15 | password: '1234',
16 | }).then(({ body }) => {
17 | window.localStorage.setItem('token', body.token)
18 | })
19 | }, {
20 | cacheAcrossSpecs,
21 | })
22 | }
23 |
24 | export function createFakeUserAndLogin() {
25 | let user
26 | before(() => {
27 | user = UserFactory.create(1)[0]
28 | })
29 |
30 | beforeEach(() => {
31 | login(user, true)
32 | })
33 |
34 | return user
35 | }
--------------------------------------------------------------------------------
/cypress/support/component.index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Components App
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/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)
--------------------------------------------------------------------------------
/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 | })
--------------------------------------------------------------------------------
/cypress/support/factory.ts:
--------------------------------------------------------------------------------
1 | import {seed} from './seed'
2 |
3 | /**
4 | * A factory makes it easy to seed the database with data.
5 | */
6 | export class Factory {
7 | static table: string | null = null
8 |
9 | static factory() {
10 | return {}
11 | }
12 |
13 | /**
14 | * Seeds a bunch of fake data into the database.
15 | *
16 | * Takes an override object as its single argument which will override the data from the factory.
17 | * If the value of one of the override fields is `{increment}` that value will be replaced with an incrementing
18 | * number through all created entities.
19 | *
20 | * @param override
21 | * @returns {[]}
22 | */
23 | static create(count = 1, override = {}, truncate = true) {
24 | const data = []
25 |
26 | for (let i = 1; i <= count; i++) {
27 | const entry = {
28 | ...this.factory(),
29 | ...override,
30 | }
31 | for (const e in entry) {
32 | if(typeof entry[e] === 'function') {
33 | entry[e] = entry[e](i)
34 | continue
35 | }
36 | if (entry[e] === '{increment}') {
37 | entry[e] = i
38 | }
39 | }
40 | data.push(entry)
41 | }
42 |
43 | seed(this.table, data, truncate)
44 |
45 | return data
46 | }
47 |
48 | static truncate() {
49 | seed(this.table, null)
50 | }
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/docker/injector.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | set -e
3 |
4 | echo "info: API URL is $VIKUNJA_API_URL"
5 | echo "info: Sentry enabled: $VIKUNJA_SENTRY_ENABLED"
6 |
7 | # Escape the variable to prevent sed from complaining
8 | VIKUNJA_API_URL="$(echo "$VIKUNJA_API_URL" | sed -r 's/([:;])/\\\1/g')"
9 | VIKUNJA_SENTRY_DSN="$(echo "$VIKUNJA_SENTRY_DSN" | sed -r 's/([:;])/\\\1/g')"
10 |
11 | sed -ri "s:^(\s*window.API_URL\s*=)\s*.+:\1 '${VIKUNJA_API_URL}':g" /usr/share/nginx/html/index.html
12 | sed -ri "s:^(\s*window.SENTRY_ENABLED\s*=)\s*.+:\1 ${VIKUNJA_SENTRY_ENABLED}:g" /usr/share/nginx/html/index.html
13 | sed -ri "s:^(\s*window.SENTRY_DSN\s*=)\s*.+:\1 '${VIKUNJA_SENTRY_DSN}':g" /usr/share/nginx/html/index.html
14 | sed -ri "s:^(\s*window.PROJECT_INFINITE_NESTING_ENABLED\s*=)\s*.+:\1 '${VIKUNJA_PROJECT_INFINITE_NESTING_ENABLED}':g" /usr/share/nginx/html/index.html
15 | sed -ri "s:^(\s*window.ALLOW_ICON_CHANGES\s*=)\s*.+:\1 ${VIKUNJA_ALLOW_ICON_CHANGES}:g" /usr/share/nginx/html/index.html
16 | sed -ri "s:^(\s*window.CUSTOM_LOGO_URL\s*=)\s*.+:\1 ${VIKUNJA_CUSTOM_LOGO_URL}:g" /usr/share/nginx/html/index.html
17 |
18 | date -uIseconds | xargs echo 'info: started at'
19 |
--------------------------------------------------------------------------------
/docker/ipv6-disable.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | set -e
3 |
4 | if [ ! -f "/proc/net/if_inet6" ]; then
5 | echo "info: IPv6 is not available! Removing IPv6 listen configuration"
6 | find /etc/nginx/conf.d -name '*.conf' -type f | \
7 | while IFS= read -r CONFIG; do
8 | sed -r '/^\s*listen\s*\[::\]:.+$/d' "$CONFIG" > "$CONFIG.temp"
9 | if ! diff -U 5 "$CONFIG" "$CONFIG.temp" > "$CONFIG.diff"; then
10 | echo "info: Removing IPv6 lines from $CONFIG" | \
11 | cat - "$CONFIG.diff"
12 | echo "# IPv6 is disabled because /proc/net/if_inet6 was not found" | \
13 | cat - "$CONFIG.temp" > "$CONFIG"
14 | else
15 | echo "info: Skipping $CONFIG because it does not have IPv6 listen"
16 | fi
17 | rm -f "$CONFIG.temp" "$CONFIG.diff"
18 | done
19 | fi
20 |
--------------------------------------------------------------------------------
/env.config.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'postcss-easings' {
2 | import postcssEasings from 'postcss-easings'
3 | export default postcssEasings
4 | }
5 |
6 | declare module 'postcss-easing-gradients' {
7 | import postcssEasingGradients from 'postcss-easing-gradients'
8 | export default postcssEasingGradients
9 | }
--------------------------------------------------------------------------------
/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 |
6 | declare module 'postcss-focus-within/browser' {
7 | import focusWithinInit from 'postcss-focus-within/browser'
8 | export default focusWithinInit
9 | }
10 |
11 | declare module 'css-has-pseudo/browser' {
12 | import cssHasPseudo from 'css-has-pseudo/browser'
13 | export default cssHasPseudo
14 | }
15 |
16 | interface ImportMetaEnv {
17 | readonly VIKUNJA_API_URL?: string
18 | readonly VIKUNJA_HTTP_PORT?: number
19 | readonly VIKUNJA_HTTPS_PORT?: number
20 |
21 | readonly VIKUNJA_SENTRY_ENABLED?: boolean
22 | readonly VIKUNJA_SENTRY_DSN?: string
23 |
24 | readonly SENTRY_AUTH_TOKEN?: string
25 | readonly SENTRY_ORG?: string
26 | readonly SENTRY_PROJECT?: string
27 |
28 | readonly VITE_IS_ONLINE: boolean
29 | }
30 |
31 | interface ImportMeta {
32 | readonly env: ImportMetaEnv
33 | }
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "nixpkgs": {
4 | "locked": {
5 | "lastModified": 1701336116,
6 | "narHash": "sha256-kEmpezCR/FpITc6yMbAh4WrOCiT2zg5pSjnKrq51h5Y=",
7 | "owner": "NixOS",
8 | "repo": "nixpkgs",
9 | "rev": "f5c27c6136db4d76c30e533c20517df6864c46ee",
10 | "type": "github"
11 | },
12 | "original": {
13 | "id": "nixpkgs",
14 | "type": "indirect"
15 | }
16 | },
17 | "root": {
18 | "inputs": {
19 | "nixpkgs": "nixpkgs"
20 | }
21 | }
22 | },
23 | "root": "root",
24 | "version": 7
25 | }
26 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "Vikunja frontend dev environment";
3 |
4 | outputs = { self, nixpkgs }:
5 | let pkgs = nixpkgs.legacyPackages.x86_64-linux;
6 | in {
7 | defaultPackage.x86_64-linux =
8 | pkgs.mkShell { buildInputs = [ pkgs.nodePackages.pnpm pkgs.cypress pkgs.git-cliff ]; };
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/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 | })
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | command = "pnpm run build"
3 | publish = "dist-preview"
4 |
5 | [[redirects]]
6 | from = "/*"
7 | to = "/index.html"
8 | status = 200
9 |
10 | [[headers]]
11 | for = "/*"
12 | [headers.values]
13 | X-Frame-Options = "DENY"
14 | X-XSS-Protection = "1; mode=block"
15 | X-Robots-Tag = "noindex"
16 |
--------------------------------------------------------------------------------
/originalMedia/audio/pop.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/audio/pop.mp3
--------------------------------------------------------------------------------
/originalMedia/audio/pop.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/audio/pop.wav
--------------------------------------------------------------------------------
/originalMedia/fonts/OpenSans-Italic[wdth,wght].ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/fonts/OpenSans-Italic[wdth,wght].ttf
--------------------------------------------------------------------------------
/originalMedia/fonts/OpenSans[wdth,wght].ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/fonts/OpenSans[wdth,wght].ttf
--------------------------------------------------------------------------------
/originalMedia/fonts/Quicksand[wght].ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/fonts/Quicksand[wght].ttf
--------------------------------------------------------------------------------
/originalMedia/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/originalMedia/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/originalMedia/icons/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/icons/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/originalMedia/icons/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/icons/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/originalMedia/icons/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/icons/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/originalMedia/icons/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/icons/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/originalMedia/icons/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/icons/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/originalMedia/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/originalMedia/icons/badge-monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/icons/badge-monochrome.png
--------------------------------------------------------------------------------
/originalMedia/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/originalMedia/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/originalMedia/icons/icon-maskable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/icons/icon-maskable.png
--------------------------------------------------------------------------------
/originalMedia/icons/msapplication-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/icons/msapplication-icon-144x144.png
--------------------------------------------------------------------------------
/originalMedia/icons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/icons/mstile-150x150.png
--------------------------------------------------------------------------------
/originalMedia/images/llama-nightscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/images/llama-nightscape.png
--------------------------------------------------------------------------------
/originalMedia/images/migration/trello.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/originalMedia/images/migration/wunderlist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/originalMedia/images/migration/wunderlist.png
--------------------------------------------------------------------------------
/patches/flexsearch@0.7.31.patch:
--------------------------------------------------------------------------------
1 | diff --git a/index.d.ts b/index.d.ts
2 | deleted file mode 100644
3 | index 9f39f41073864b83968bdaa242ac4e3c3149685a..0000000000000000000000000000000000000000
4 | diff --git a/package.json b/package.json
5 | index 8968f5bf8010ff194240591c8b83299f7328e79d..6d84b6f590a841b129ed8b3860cb786df5a185c0 100644
6 | --- a/package.json
7 | +++ b/package.json
8 | @@ -22,8 +22,6 @@
9 | },
10 | "main": "dist/flexsearch.bundle.js",
11 | "browser": "dist/flexsearch.bundle.js",
12 | - "module": "dist/module/index.js",
13 | - "types": "./index.d.ts",
14 | "preferGlobal": false,
15 | "repository": {
16 | "type": "git",
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/public/images/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/images/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/public/images/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/images/icons/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/public/images/icons/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/public/images/icons/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/public/images/icons/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/public/images/icons/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/public/images/icons/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/public/images/icons/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/public/images/icons/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/public/images/icons/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/public/images/icons/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/public/images/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/public/images/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/images/icons/badge-monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/public/images/icons/badge-monochrome.png
--------------------------------------------------------------------------------
/public/images/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/public/images/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/public/images/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/public/images/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/public/images/icons/icon-maskable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/public/images/icons/icon-maskable.png
--------------------------------------------------------------------------------
/public/images/icons/msapplication-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/public/images/icons/msapplication-icon-144x144.png
--------------------------------------------------------------------------------
/public/images/icons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/public/images/icons/mstile-150x150.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "labels": ["dependencies"],
4 | "extends": [
5 | "config:js-app"
6 | ],
7 | "hostRules": [
8 | {
9 | "timeout": 600000
10 | }
11 | ],
12 | "packageRules": [
13 | {
14 | "matchPackageNames": ["happy-dom"],
15 | "extends": ["schedule:weekly"]
16 | },
17 | {
18 | "groupName": "caniuse-and-related",
19 | "matchPackageNames": ["caniuse-lite", "browserslist"],
20 | "extends": ["schedule:weekly"]
21 | },
22 | {
23 | "groupName": "vueuse",
24 | "matchPackagePrefixes": [
25 | "@vueuse/"
26 | ]
27 | },
28 | {
29 | "groupName": "histoire",
30 | "matchPackagePrefixes": [
31 | "@histoire/",
32 | "histoire"
33 | ]
34 | },
35 | {
36 | "groupName": "tiptap",
37 | "matchPackagePrefixes": [
38 | "@tiptap/",
39 | "tiptap"
40 | ]
41 | },
42 | {
43 | "matchDepTypes": ["devDependencies"],
44 | "groupName": "dev-dependencies",
45 | "extends": ["schedule:daily"]
46 | }
47 | ]
48 | }
--------------------------------------------------------------------------------
/scripts/deploy-preview-netlify.mjs.sha384:
--------------------------------------------------------------------------------
1 | 4a7c1293c7b12e9ab476cdf35251a407c6a1cd005d22c06df994222cccfb25cde5f47d15866a098c9d739778fee4dc19 ./scripts/deploy-preview-netlify.mjs
2 |
--------------------------------------------------------------------------------
/src/assets/audio/pop.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/src/assets/audio/pop.mp3
--------------------------------------------------------------------------------
/src/assets/checkbox.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-BoldItalic_3ff98862.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/src/assets/fonts/OpenSans-BoldItalic_3ff98862.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-Bold_eb52363b.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/src/assets/fonts/OpenSans-Bold_eb52363b.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-Italic[wght]_c9a8fe68.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/src/assets/fonts/OpenSans-Italic[wght]_c9a8fe68.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-RegularItalic_48244a7a.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/src/assets/fonts/OpenSans-RegularItalic_48244a7a.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-Regular_d0acb717.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/src/assets/fonts/OpenSans-Regular_d0acb717.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans[wght]_54a65da5.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/src/assets/fonts/OpenSans[wght]_54a65da5.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Quicksand-Bold_20b26f76.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/src/assets/fonts/Quicksand-Bold_20b26f76.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Quicksand-Regular_3e913e7e.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/src/assets/fonts/Quicksand-Regular_3e913e7e.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Quicksand-SemiBold_be48a442.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/src/assets/fonts/Quicksand-SemiBold_be48a442.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Quicksand[wght]_87bdcc7f.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/src/assets/fonts/Quicksand[wght]_87bdcc7f.woff2
--------------------------------------------------------------------------------
/src/assets/llama-nightscape.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/src/assets/llama-nightscape.jpg
--------------------------------------------------------------------------------
/src/assets/no-auth-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/src/assets/no-auth-image.jpg
--------------------------------------------------------------------------------
/src/components/date/dateRanges.ts:
--------------------------------------------------------------------------------
1 | export const DATE_RANGES = {
2 | // Format:
3 | // Key is the title, as a translation string, the first entry of the value array
4 | // is the "from" date, the second one is the "to" date.
5 | 'today': ['now/d', 'now/d+1d'],
6 |
7 | 'lastWeek': ['now/w-1w', 'now/w-2w'],
8 | 'thisWeek': ['now/w', 'now/w+1w'],
9 | 'restOfThisWeek': ['now', 'now/w+1w'],
10 | 'nextWeek': ['now/w+1w', 'now/w+2w'],
11 | 'next7Days': ['now', 'now+7d'],
12 |
13 | 'lastMonth': ['now/M-1M', 'now/M-2M'],
14 | 'thisMonth': ['now/M', 'now/M+1M'],
15 | 'restOfThisMonth': ['now', 'now/M+1M'],
16 | 'nextMonth': ['now/M+1M', 'now/M+2M'],
17 | 'next30Days': ['now', 'now+30d'],
18 |
19 | 'thisYear': ['now/y', 'now/y+1y'],
20 | 'restOfThisYear': ['now', 'now/y+1y'],
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/date/datemathHelp.story.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/home/DemoMode.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
16 |
17 | {{ $t('demo.title') }}
18 | {{ $t('demo.everythingWillBeDeleted') }}
19 |
20 |
hide = true"
23 | >
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/components/home/Logo.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
23 |
![Vikunja]()
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/components/home/PoweredByLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
15 |
16 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/components/input/Button.story.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
13 | Order pizza!
14 |
15 |
16 |
17 |
18 |
22 | Order spaghetti!
23 |
24 |
25 |
26 |
27 |
31 | Order tortellini!
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/components/input/ColorPicker.story.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/components/input/SimpleButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
27 |
--------------------------------------------------------------------------------
/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 | })
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/components/misc/ButtonLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/src/components/misc/Card.story.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | Card content
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/components/misc/Done.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 | {{ $t('task.attributes.done') }}
8 |
9 |
10 |
11 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/components/misc/OpenQuickActions.vue:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/components/misc/ProgressBar.story.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/components/misc/colorBubble.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
15 |
16 |
--------------------------------------------------------------------------------
/src/components/misc/error.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 | {{ $t('loadingError.tryAgain') }}
9 |
10 |
11 | {{ $t('loadingError.contact') }}
12 |
13 |
14 |
15 |
16 |
17 |
22 |
23 |
31 |
--------------------------------------------------------------------------------
/src/components/misc/legal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | {{ $t('navigation.imprint') }}
8 |
9 | |
10 |
14 | {{ $t('navigation.privacy') }}
15 |
16 |
17 |
18 |
19 |
29 |
30 |
--------------------------------------------------------------------------------
/src/components/misc/loading.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
13 |
14 |
21 |
22 |
--------------------------------------------------------------------------------
/src/components/misc/nothing.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/components/misc/notification.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
16 |
20 | {{ item.title }}
21 |
22 |
23 |
27 | {{ t }}
28 |
29 |
30 |
34 |
42 | {{ action.title }}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/components/misc/popup.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
23 |
24 |
25 |
57 |
58 |
74 |
--------------------------------------------------------------------------------
/src/components/misc/shortcut.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
10 | {{ k }}
11 | {{ combination }}
12 |
13 |
14 |
15 |
16 |
32 |
33 |
--------------------------------------------------------------------------------
/src/components/tasks/partials/date-table-cell.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 | |
7 |
8 |
9 |
19 |
--------------------------------------------------------------------------------
/src/components/tasks/partials/label.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
15 | {{ label.title }}
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/components/tasks/partials/labels.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
24 |
25 |
--------------------------------------------------------------------------------
/src/components/tasks/partials/percentDoneSelect.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
41 |
42 |
43 |
44 |
66 |
--------------------------------------------------------------------------------
/src/components/tasks/partials/reminders.story.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/components/tasks/partials/sort.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
12 |
16 |
17 |
18 |
19 |
32 |
--------------------------------------------------------------------------------
/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 | isActive.value
9 | ? document.body.classList.add(className)
10 | : document.body.classList.remove(className)
11 | })
12 |
13 | tryOnBeforeUnmount(() => isActive.value && document.body.classList.remove(className))
14 |
15 | return isActive
16 | }
--------------------------------------------------------------------------------
/src/composables/useCopyToClipboard.ts:
--------------------------------------------------------------------------------
1 | import {error} from '@/message'
2 | import {useI18n} from 'vue-i18n'
3 |
4 | export function useCopyToClipboard() {
5 | const {t} = useI18n({useScope: 'global'})
6 |
7 | function fallbackCopyTextToClipboard(text: string) {
8 | const textArea = document.createElement('textarea')
9 | textArea.value = text
10 |
11 | // Avoid scrolling to bottom
12 | textArea.style.top = '0'
13 | textArea.style.left = '0'
14 | textArea.style.position = 'fixed'
15 |
16 | document.body.appendChild(textArea)
17 | textArea.focus()
18 | textArea.select()
19 |
20 | try {
21 | // NOTE: the execCommand is deprecated but as of 2022_09
22 | // widely supported and works without https
23 | const successful = document.execCommand('copy')
24 | if (!successful) {
25 | throw new Error()
26 | }
27 | } catch (err) {
28 | error(t('misc.copyError'))
29 | }
30 |
31 | document.body.removeChild(textArea)
32 | }
33 |
34 | return async (text: string) => {
35 | if (!navigator.clipboard) {
36 | fallbackCopyTextToClipboard(text)
37 | return
38 | }
39 | try {
40 | await navigator.clipboard.writeText(text)
41 | } catch(e) {
42 | error(t('misc.copyError'))
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/src/composables/useDaytimeSalutation.ts:
--------------------------------------------------------------------------------
1 | import {computed, onActivated, ref} from 'vue'
2 | import {useI18n} from 'vue-i18n'
3 |
4 | import {useAuthStore} from '@/stores/auth'
5 | import {hourToDaytime} from '@/helpers/hourToDaytime'
6 |
7 | export type Daytime = 'night' | 'morning' | 'day' | 'evening'
8 |
9 | export function useDaytimeSalutation() {
10 | const {t} = useI18n({useScope: 'global'})
11 | const now = ref(new Date())
12 | onActivated(() => now.value = new Date())
13 | const authStore = useAuthStore()
14 |
15 | const name = computed(() => authStore.userDisplayName)
16 | const daytime = computed(() => hourToDaytime(now.value))
17 |
18 | const salutations = {
19 | 'night': () => t('home.welcomeNight', {username: name.value}),
20 | 'morning': () => t('home.welcomeMorning', {username: name.value}),
21 | 'day': () => t('home.welcomeDay', {username: name.value}),
22 | 'evening': () => t('home.welcomeEvening', {username: name.value}),
23 | } as Record string>
24 |
25 | return computed(() => name.value ? salutations[daytime.value]() : undefined)
26 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/composables/useTitle.ts:
--------------------------------------------------------------------------------
1 | import {computed} from 'vue'
2 | import type {Ref} from 'vue'
3 |
4 | import {useTitle as useTitleVueUse, toRef} from '@vueuse/core'
5 |
6 | type UseTitleParameters = Parameters
7 |
8 | export function useTitle(...args: UseTitleParameters) {
9 |
10 | const [newTitle, ...restArgs] = args
11 |
12 | const pageTitle = toRef(newTitle) as Ref
13 |
14 | const completeTitle = computed(() =>
15 | (typeof pageTitle.value === 'undefined' || pageTitle.value === '')
16 | ? 'Vikunja'
17 | : `${pageTitle.value} | Vikunja`,
18 | )
19 |
20 | return useTitleVueUse(completeTitle, ...restArgs)
21 | }
--------------------------------------------------------------------------------
/src/constants/date.ts:
--------------------------------------------------------------------------------
1 | export const DATEFNS_DATE_FORMAT_KEBAB = 'yyyy-LL-dd'
2 |
3 | export const SECONDS_A_MINUTE = 60
4 | export const SECONDS_A_HOUR = SECONDS_A_MINUTE * 60
5 | export const SECONDS_A_DAY = SECONDS_A_HOUR * 24
6 | export const SECONDS_A_WEEK = SECONDS_A_DAY * 7
7 | export const SECONDS_A_MONTH = SECONDS_A_DAY * 30
8 | export const SECONDS_A_YEAR = SECONDS_A_DAY * 365
9 |
10 | export const MILLISECONDS_A_SECOND = 1000
11 | export const MILLISECONDS_A_MINUTE = SECONDS_A_MINUTE * MILLISECONDS_A_SECOND
12 | export const MILLISECONDS_A_HOUR = SECONDS_A_HOUR * MILLISECONDS_A_SECOND
13 | export const MILLISECONDS_A_DAY = SECONDS_A_DAY * MILLISECONDS_A_SECOND
14 | export const MILLISECONDS_A_WEEK = SECONDS_A_WEEK * MILLISECONDS_A_SECOND
--------------------------------------------------------------------------------
/src/constants/linkShareHash.ts:
--------------------------------------------------------------------------------
1 | export const LINK_SHARE_HASH_PREFIX = '#share-auth-token='
2 |
--------------------------------------------------------------------------------
/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]
--------------------------------------------------------------------------------
/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]
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/helpers/calculateItemPosition.ts:
--------------------------------------------------------------------------------
1 | export const calculateItemPosition = (positionBefore: number | null, positionAfter: number | null): number => {
2 | if (positionBefore === null) {
3 | if (positionAfter === null) {
4 | return 0
5 | }
6 |
7 | // If there is no task after it, we just add 2^16 to the last position to have enough room in the future
8 | return positionAfter / 2
9 | }
10 |
11 | // If there is no task after it, we just add 2^16 to the last position to have enough room in the future
12 | if (positionAfter === null) {
13 | return positionBefore + Math.pow(2, 16)
14 | }
15 |
16 | // If we have both a task before and after it, we acually calculate the position
17 | return positionBefore + (positionAfter - positionBefore) / 2
18 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/helpers/canNestProjectDeeper.ts:
--------------------------------------------------------------------------------
1 | export function canNestProjectDeeper(level: number) {
2 | if (level < 2) {
3 | return true
4 | }
5 |
6 | return level >= 2 && window.PROJECT_INFINITE_NESTING_ENABLED
7 | }
--------------------------------------------------------------------------------
/src/helpers/checklistFromText.ts:
--------------------------------------------------------------------------------
1 | interface CheckboxStatistics {
2 | total: number
3 | checked: number
4 | }
5 |
6 | interface MatchedCheckboxes {
7 | checked: number[]
8 | unchecked: number[]
9 | }
10 |
11 | const getCheckboxesInText = (text: string): MatchedCheckboxes => {
12 | const regex = /data-checked="(true|false)"/g
13 | let match
14 | const checkboxes: MatchedCheckboxes = {
15 | checked: [],
16 | unchecked: [],
17 | }
18 |
19 | while ((match = regex.exec(text)) !== null) {
20 | if (match[1] === 'true') {
21 | checkboxes.checked.push(match.index)
22 | } else {
23 | checkboxes.unchecked.push(match.index)
24 | }
25 | }
26 |
27 | return checkboxes
28 | }
29 |
30 | /**
31 | * Returns the indices where checkboxes start and end in the given text.
32 | *
33 | * @param text
34 | */
35 | export const findCheckboxesInText = (text: string): number[] => {
36 | const checkboxes = getCheckboxesInText(text)
37 |
38 | return [
39 | ...checkboxes.checked,
40 | ...checkboxes.unchecked,
41 | ].sort((a, b) => a < b ? -1 : 1)
42 | }
43 |
44 | export const getChecklistStatistics = (text: string): CheckboxStatistics => {
45 | const checkboxes = getCheckboxesInText(text)
46 |
47 | return {
48 | total: checkboxes.checked.length + checkboxes.unchecked.length,
49 | checked: checkboxes.checked.length,
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/helpers/editorContentEmpty.ts:
--------------------------------------------------------------------------------
1 | export function isEditorContentEmpty(content: string): boolean {
2 | return content === '' || content === ''
3 | }
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/helpers/flatpickrLanguage.ts:
--------------------------------------------------------------------------------
1 | import {useAuthStore} from '@/stores/auth'
2 | import FlatpickrLanguages from 'flatpickr/dist/l10n'
3 | import type { CustomLocale, key } from 'flatpickr/dist/types/locale'
4 |
5 | export function getFlatpickrLanguage(): CustomLocale {
6 | const authStore = useAuthStore()
7 | const lang = authStore.settings.language
8 | const langPair = lang.split('-')
9 | let language = FlatpickrLanguages[lang === 'vi-vn' ? 'vn' : 'en']
10 | if (langPair.length > 0 && FlatpickrLanguages[langPair[0] as key] !== undefined) {
11 | language = FlatpickrLanguages[langPair[0] as key]
12 | }
13 | language.firstDayOfWeek = authStore.settings.weekStart ?? language.firstDayOfWeek
14 | return language
15 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/helpers/hourToDaytime.test.ts:
--------------------------------------------------------------------------------
1 | import {describe, it, expect} from 'vitest'
2 | import {hourToDaytime} from "./hourToDaytime"
3 |
4 | function dateWithHour(hours: number): Date {
5 | const newDate = new Date()
6 | newDate.setHours(hours, 0, 0,0 )
7 | return newDate
8 | }
9 |
10 | describe('Salutation', () => {
11 | it('shows the right salutation in the night', () => {
12 | const salutation = hourToDaytime(dateWithHour(4))
13 | expect(salutation).toBe('night')
14 | })
15 | it('shows the right salutation in the morning', () => {
16 | const salutation = hourToDaytime(dateWithHour(8))
17 | expect(salutation).toBe('morning')
18 | })
19 | it('shows the right salutation in the day', () => {
20 | const salutation = hourToDaytime(dateWithHour(13))
21 | expect(salutation).toBe('day')
22 | })
23 | it('shows the right salutation in the night', () => {
24 | const salutation = hourToDaytime(dateWithHour(20))
25 | expect(salutation).toBe('evening')
26 | })
27 | it('shows the right salutation in the night again', () => {
28 | const salutation = hourToDaytime(dateWithHour(23))
29 | expect(salutation).toBe('night')
30 | })
31 | })
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/helpers/inputPrompt.ts:
--------------------------------------------------------------------------------
1 | import {createRandomID} from '@/helpers/randomId'
2 | import tippy from 'tippy.js'
3 | import {nextTick} from 'vue'
4 | import {eventToHotkeyString} from '@github/hotkey'
5 |
6 | export default function inputPrompt(pos: ClientRect, oldValue: string = ''): Promise {
7 | return new Promise((resolve) => {
8 | const id = 'link-input-' + createRandomID()
9 |
10 | const linkPopup = tippy('body', {
11 | getReferenceClientRect: () => pos,
12 | appendTo: () => document.body,
13 | content: ``,
14 | showOnCreate: true,
15 | interactive: true,
16 | trigger: 'manual',
17 | placement: 'top-start',
18 | allowHTML: true,
19 | })
20 |
21 | linkPopup[0].show()
22 |
23 | nextTick(() => document.getElementById(id)?.focus())
24 |
25 | document.getElementById(id)?.addEventListener('keydown', event => {
26 | const hotkeyString = eventToHotkeyString(event)
27 | if (hotkeyString !== 'Enter') {
28 | return
29 | }
30 |
31 | const url = event.target.value
32 |
33 | resolve(url)
34 |
35 | linkPopup[0].hide()
36 | })
37 |
38 | })
39 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/helpers/playPop.ts:
--------------------------------------------------------------------------------
1 | import popSoundFile from '@/assets/audio/pop.mp3'
2 |
3 | export const playSoundWhenDoneKey = 'playSoundWhenTaskDone'
4 |
5 | export function playPopSound() {
6 | try {
7 | const popSound = new Audio(popSoundFile)
8 | popSound.play()
9 | } catch (e) {
10 | console.error('Could not play pop sound:', e)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/helpers/redirectToProvider.ts:
--------------------------------------------------------------------------------
1 | import {createRandomID} from '@/helpers/randomId'
2 | import type {IProvider} from '@/types/IProvider'
3 | import {parseURL} from 'ufo'
4 |
5 | export function getRedirectUrlFromCurrentFrontendPath(provider: IProvider): string {
6 | // We're not using the redirect url provided by the server to allow redirects when using the electron app.
7 | // The implications are not quite clear yet hence the logic to pass in another redirect url still exists.
8 | const url = parseURL(window.location.href)
9 | return `${url.protocol}//${url.host}/auth/openid/${provider.key}`
10 | }
11 |
12 | export const redirectToProvider = (provider: IProvider) => {
13 |
14 | console.log({provider})
15 |
16 | const redirectUrl = getRedirectUrlFromCurrentFrontendPath(provider)
17 | const state = createRandomID(24)
18 | localStorage.setItem('state', state)
19 |
20 | window.location.href = `${provider.authUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUrl}&response_type=code&scope=openid email profile&state=${state}`
21 | }
22 | export const redirectToProviderOnLogout = (provider: IProvider) => {
23 | if (provider.logoutUrl.length > 0) {
24 | window.location.href = `${provider.logoutUrl}`
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/helpers/setTitle.ts:
--------------------------------------------------------------------------------
1 | export function setTitle(title : undefined | string) {
2 | document.title = (typeof title === 'undefined' || title === '')
3 | ? 'Vikunja'
4 | : `${title} | Vikunja`
5 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/helpers/time/parseBooleanProp.ts:
--------------------------------------------------------------------------------
1 | export function parseBooleanProp(booleanProp: string | undefined) {
2 | return (booleanProp === 'false' || booleanProp === '0')
3 | ? false
4 | : Boolean(booleanProp)
5 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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(e) {
27 | // ignore nonsense route queries
28 | return
29 | }
30 | }
--------------------------------------------------------------------------------
/src/helpers/time/parseKebabDate.ts:
--------------------------------------------------------------------------------
1 | import {parse} from 'date-fns'
2 | import {DATEFNS_DATE_FORMAT_KEBAB} from '@/constants/date'
3 | import type {DateKebab} from '@/types/DateKebab'
4 |
5 | export function parseKebabDate(date: DateKebab): Date {
6 | return parse(date, DATEFNS_DATE_FORMAT_KEBAB, new Date())
7 | }
--------------------------------------------------------------------------------
/src/helpers/time/period.ts:
--------------------------------------------------------------------------------
1 | import {
2 | SECONDS_A_DAY,
3 | SECONDS_A_HOUR,
4 | SECONDS_A_MINUTE,
5 | SECONDS_A_WEEK,
6 | } from '@/constants/date'
7 |
8 | export type PeriodUnit = 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'months' | 'years'
9 |
10 | /**
11 | * Convert time period given as seconds to days, hour, minutes, seconds
12 | */
13 | export function secondsToPeriod(seconds: number): { unit: PeriodUnit, amount: number } {
14 | if (seconds % SECONDS_A_DAY === 0) {
15 | if (seconds % SECONDS_A_WEEK === 0) {
16 | return {unit: 'weeks', amount: seconds / SECONDS_A_WEEK}
17 | } else {
18 | return {unit: 'days', amount: seconds / SECONDS_A_DAY}
19 | }
20 | }
21 |
22 | return {
23 | unit: 'hours',
24 | amount: seconds / SECONDS_A_HOUR,
25 | }
26 | }
27 |
28 | /**
29 | * Convert time period of days, hour, minutes, seconds to duration in seconds
30 | */
31 | export function periodToSeconds(period: number, unit: PeriodUnit): number {
32 | switch (unit) {
33 | case 'minutes':
34 | return period * SECONDS_A_MINUTE
35 | case 'hours':
36 | return period * SECONDS_A_HOUR
37 | case 'days':
38 | return period * SECONDS_A_DAY
39 | case 'weeks':
40 | return period * SECONDS_A_WEEK
41 | }
42 |
43 | return 0
44 | }
45 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/histoire.setup.ts:
--------------------------------------------------------------------------------
1 | import './polyfills'
2 | import {defineSetupVue3} from '@histoire/plugin-vue'
3 | import {i18n} from './i18n'
4 |
5 | // import './histoire.css' // Import global CSS
6 | import './styles/global.scss'
7 |
8 | import {createPinia} from 'pinia'
9 |
10 | import cypress from '@/directives/cypress'
11 |
12 | import FontAwesomeIcon from '@/components/misc/Icon'
13 | import XButton from '@/components/input/button.vue'
14 | import Modal from '@/components/misc/modal.vue'
15 | import Card from '@/components/misc/card.vue'
16 |
17 | export const setupVue3 = defineSetupVue3(({ app }) => {
18 | // Add Pinia store
19 | const pinia = createPinia()
20 | app.use(pinia)
21 | app.use(i18n)
22 |
23 | app.directive('cy', cypress)
24 |
25 | app.component('Icon', FontAwesomeIcon)
26 | app.component('XButton', XButton)
27 | app.component('Modal', Modal)
28 | app.component('Card', Card)
29 | })
--------------------------------------------------------------------------------
/src/indexes/index.ts:
--------------------------------------------------------------------------------
1 | import {Document} from 'flexsearch'
2 |
3 | export interface withId {
4 | id: number,
5 | }
6 |
7 | const indexes: { [k: string]: Document } = {}
8 |
9 | export const createNewIndexer = (name: string, fieldsToIndex: string[]) => {
10 | if (typeof indexes[name] === 'undefined') {
11 | indexes[name] = new Document({
12 | tokenize: 'full',
13 | document: {
14 | id: 'id',
15 | index: fieldsToIndex,
16 | },
17 | })
18 | }
19 |
20 | const index = indexes[name]
21 |
22 | function add(item: withId) {
23 | return index.add(item.id, item)
24 | }
25 |
26 | function remove(item: withId) {
27 | return index.remove(item.id)
28 | }
29 |
30 | function update(item: withId) {
31 | return index.update(item.id, item)
32 | }
33 |
34 | function search(query: string | null) {
35 | if (query === '' || query === null) {
36 | return null
37 | }
38 |
39 | return index.search(query)
40 | ?.flatMap(r => r.result)
41 | .filter((value, index, self) => self.indexOf(value) === index) as number[]
42 | || null
43 | }
44 |
45 | return {
46 | add,
47 | remove,
48 | update,
49 | search,
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/message/index.ts:
--------------------------------------------------------------------------------
1 | import {i18n} from '@/i18n'
2 | import {notify} from '@kyvg/vue3-notification'
3 |
4 | export function getErrorText(r): string {
5 | const data = r?.reason?.response?.data || r?.response?.data
6 |
7 | if (data?.code) {
8 | const path = `error.${data.code}`
9 | const message = i18n.global.t(path)
10 |
11 | // If message and path are equal no translation exists for that error code
12 | if (path !== message) {
13 | return message
14 | }
15 | }
16 |
17 | let message = data?.message || r.message
18 |
19 | if (typeof r.cause?.message !== 'undefined') {
20 | message += ' ' + r.cause.message
21 | }
22 |
23 | return message
24 | }
25 |
26 | export interface Action {
27 | title: string,
28 | callback: () => void,
29 | }
30 |
31 | export function error(e, actions: Action[] = []) {
32 | notify({
33 | type: 'error',
34 | title: i18n.global.t('error.error'),
35 | text: [getErrorText(e)],
36 | actions: actions,
37 | })
38 | }
39 |
40 | export function success(e, actions: Action[] = []) {
41 | notify({
42 | type: 'success',
43 | title: i18n.global.t('error.success'),
44 | text: [getErrorText(e)],
45 | data: {
46 | actions: actions,
47 | },
48 | })
49 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/modelTypes/IBucket.ts:
--------------------------------------------------------------------------------
1 | import type {IAbstract} from './IAbstract'
2 | import type {IUser} from './IUser'
3 | import type {ITask} from './ITask'
4 |
5 | export interface IBucket extends IAbstract {
6 | id: number
7 | title: string
8 | projectId: number
9 | limit: number
10 | tasks: ITask[]
11 | position: number
12 | count: number
13 |
14 | createdBy: IUser
15 | created: Date
16 | updated: Date
17 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/modelTypes/IEmailUpdate.ts:
--------------------------------------------------------------------------------
1 | import type {IAbstract} from './IAbstract'
2 |
3 | export interface IEmailUpdate extends IAbstract {
4 | newEmail: string
5 | password: string
6 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/modelTypes/IPasswordUpdate.ts:
--------------------------------------------------------------------------------
1 | import type {IAbstract} from './IAbstract'
2 |
3 | export interface IPasswordUpdate extends IAbstract {
4 | newPassword: string
5 | oldPassword: string
6 | }
--------------------------------------------------------------------------------
/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 |
6 |
7 | export interface IProject extends IAbstract {
8 | id: number
9 | title: string
10 | description: string
11 | owner: IUser
12 | tasks: ITask[]
13 | isArchived: boolean
14 | hexColor: string
15 | identifier: string
16 | backgroundInformation: unknown | null // FIXME: improve type
17 | isFavorite: boolean
18 | subscription: ISubscription
19 | position: number
20 | backgroundBlurHash: string
21 | parentProjectId: number
22 | doneBucketId: number
23 | defaultBucketId: number
24 |
25 | created: Date
26 | updated: Date
27 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/modelTypes/ISavedFilter.ts:
--------------------------------------------------------------------------------
1 | import type {IAbstract} from './IAbstract'
2 | import type {IUser} from './IUser'
3 | import type {IFilter} from '@/types/IFilter'
4 |
5 | export interface ISavedFilter extends IAbstract {
6 | id: number
7 | title: string
8 | description: string
9 | filters: IFilter
10 |
11 | owner: IUser
12 | created: Date
13 | updated: Date
14 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/modelTypes/ITaskComment.ts:
--------------------------------------------------------------------------------
1 | import type {IAbstract} from './IAbstract'
2 | import type {IUser} from './IUser'
3 | import type {ITask} from './ITask'
4 |
5 | export interface ITaskComment extends IAbstract {
6 | id: number
7 | taskId: ITask['id']
8 | comment: string
9 | author: IUser
10 |
11 | created: Date
12 | updated: Date
13 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
13 | createdBy: IUser
14 | created: Date
15 | updated: Date
16 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/modelTypes/IUserSettings.ts:
--------------------------------------------------------------------------------
1 |
2 | import type {IAbstract} from './IAbstract'
3 | import type {IProject} from './IProject'
4 | import type {PrefixMode} from '@/modules/parseTaskText'
5 | import type {BasicColorSchema} from '@vueuse/core'
6 | import type {SupportedLocale} from '@/i18n'
7 |
8 | export interface IFrontendSettings {
9 | playSoundWhenDone: boolean
10 | quickAddMagicMode: PrefixMode
11 | colorSchema: BasicColorSchema
12 | filterIdUsedOnOverview: IProject['id'] | null
13 | }
14 |
15 | export interface IUserSettings extends IAbstract {
16 | name: string
17 | emailRemindersEnabled: boolean
18 | discoverableByName: boolean
19 | discoverableByEmail: boolean
20 | overdueTasksRemindersEnabled: boolean
21 | overdueTasksRemindersTime: string | Date
22 | defaultProjectId: undefined | IProject['id']
23 | weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6
24 | timezone: string
25 | language: SupportedLocale
26 | frontendSettings: IFrontendSettings
27 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 = ['.jpg', '.png', '.bmp', '.gif']
9 |
10 | export default class AttachmentModel extends AbstractModel implements IAttachment {
11 | id = 0
12 | taskId = 0
13 | createdBy: IUser = UserModel
14 | file: IFile = FileModel
15 | created: Date = null
16 |
17 | constructor(data: Partial) {
18 | super()
19 | this.assignData(data)
20 |
21 | this.createdBy = new UserModel(this.createdBy)
22 | this.file = new FileModel(this.file)
23 | this.created = new Date(this.created)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | this.token = localStorage.getItem('passwordResetToken')
15 | }
16 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/models/savedFilter.ts:
--------------------------------------------------------------------------------
1 | import AbstractModel from './abstractModel'
2 | import UserModel from '@/models/user'
3 |
4 | import type {ISavedFilter} from '@/modelTypes/ISavedFilter'
5 | import type {IUser} from '@/modelTypes/IUser'
6 |
7 | export default class SavedFilterModel extends AbstractModel implements ISavedFilter {
8 | id = 0
9 | title = ''
10 | description = ''
11 | filters: ISavedFilter['filters'] = {
12 | sortBy: ['done', 'id'],
13 | orderBy: ['asc', 'desc'],
14 | filterBy: ['done'],
15 | filterValue: ['false'],
16 | filterComparator: ['equals'],
17 | filterConcat: 'and',
18 | filterIncludeNulls: true,
19 | }
20 |
21 | owner: IUser = {}
22 | created: Date = null
23 | updated: Date = null
24 |
25 | constructor(data: Partial = {}) {
26 | super()
27 | this.assignData(data)
28 |
29 | this.owner = new UserModel(this.owner)
30 |
31 | this.created = new Date(this.created)
32 | this.updated = new Date(this.updated)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | created: Date = null
15 | updated: Date = null
16 |
17 | constructor(data: Partial = {}) {
18 | super()
19 | this.assignData(data)
20 |
21 | this.author = new UserModel(this.author)
22 | this.created = new Date(this.created)
23 | this.updated = new Date(this.updated)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/models/team.ts:
--------------------------------------------------------------------------------
1 | import AbstractModel from './abstractModel'
2 | import UserModel from './user'
3 | import TeamMemberModel from './teamMember'
4 |
5 | import {RIGHTS, type Right} from '@/constants/rights'
6 | import type {ITeam} from '@/modelTypes/ITeam'
7 | import type {ITeamMember} from '@/modelTypes/ITeamMember'
8 | import type {IUser} from '@/modelTypes/IUser'
9 |
10 | export default class TeamModel extends AbstractModel implements ITeam {
11 | id = 0
12 | name = ''
13 | description = ''
14 | members: ITeamMember[] = []
15 | right: Right = RIGHTS.READ
16 |
17 | createdBy: IUser = {} // FIXME: seems wrong
18 | created: Date = null
19 | updated: Date = null
20 |
21 | constructor(data: Partial = {}) {
22 | super()
23 | this.assignData(data)
24 |
25 | // Make the members to usermodels
26 | this.members = this.members.map(m => {
27 | return new TeamMemberModel(m)
28 | })
29 | this.createdBy = new UserModel(this.createdBy)
30 |
31 | this.created = new Date(this.created)
32 | this.updated = new Date(this.updated)
33 | }
34 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/models/user.ts:
--------------------------------------------------------------------------------
1 | import AbstractModel from './abstractModel'
2 | import UserSettingsModel from '@/models/userSettings'
3 |
4 | import { AUTH_TYPES, type IUser, type AuthType } from '@/modelTypes/IUser'
5 | import type { IUserSettings } from '@/modelTypes/IUserSettings'
6 |
7 | export function getAvatarUrl(user: IUser, size = 50) {
8 | return `${window.API_URL}/avatar/${user.username}?size=${size}`
9 | }
10 |
11 | export function getDisplayName(user: IUser) {
12 | if (user.name !== '') {
13 | return user.name
14 | }
15 |
16 | return user.username
17 | }
18 |
19 | export default class UserModel extends AbstractModel implements IUser {
20 | id = 0
21 | email = ''
22 | username = ''
23 | name = ''
24 | exp = 0
25 | type: AuthType = AUTH_TYPES.UNKNOWN
26 |
27 | created: Date
28 | updated: Date
29 | settings: IUserSettings
30 |
31 | isLocalUser: boolean
32 | deletionScheduledAt: null
33 |
34 | constructor(data: Partial = {}) {
35 | super()
36 | this.assignData(data)
37 |
38 | this.created = new Date(this.created)
39 | this.updated = new Date(this.updated)
40 |
41 | this.settings = new UserSettingsModel(this.settings || {})
42 | }
43 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/models/userSettings.ts:
--------------------------------------------------------------------------------
1 | import AbstractModel from './abstractModel'
2 |
3 | import type {IFrontendSettings, IUserSettings} from '@/modelTypes/IUserSettings'
4 | import {getBrowserLanguage} from '@/i18n'
5 | import {PrefixMode} from '@/modules/parseTaskText'
6 |
7 | export default class UserSettingsModel extends AbstractModel implements IUserSettings {
8 | name = ''
9 | emailRemindersEnabled = true
10 | discoverableByName = false
11 | discoverableByEmail = false
12 | overdueTasksRemindersEnabled = true
13 | overdueTasksRemindersTime = undefined
14 | defaultProjectId = undefined
15 | weekStart = 0 as IUserSettings['weekStart']
16 | timezone = ''
17 | language = getBrowserLanguage()
18 | frontendSettings: IFrontendSettings = {
19 | playSoundWhenDone: true,
20 | quickAddMagicMode: PrefixMode.Default,
21 | colorSchema: 'auto',
22 | }
23 |
24 | constructor(data: Partial = {}) {
25 | super()
26 | this.assignData(data)
27 | }
28 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/modules/projectHistory.ts:
--------------------------------------------------------------------------------
1 | export interface ProjectHistory {
2 | id: number;
3 | }
4 |
5 | export function getHistory(): ProjectHistory[] {
6 | const savedHistory = localStorage.getItem('projectHistory')
7 | if (savedHistory === null) {
8 | return []
9 | }
10 |
11 | return JSON.parse(savedHistory)
12 | }
13 |
14 | function saveHistory(history: ProjectHistory[]) {
15 | if (history.length === 0) {
16 | localStorage.removeItem('projectHistory')
17 | return
18 | }
19 |
20 | localStorage.setItem('projectHistory', JSON.stringify(history))
21 | }
22 |
23 | export function saveProjectToHistory(project: ProjectHistory) {
24 | const history: ProjectHistory[] = getHistory()
25 |
26 | // Remove the element if it already exists in history, preventing duplicates and essentially moving it to the beginning
27 | history.forEach((l, i) => {
28 | if (l.id === project.id) {
29 | history.splice(i, 1)
30 | }
31 | })
32 |
33 | // Add the new project to the beginning of the project
34 | history.unshift(project)
35 |
36 | if (history.length > 5) {
37 | history.pop()
38 | }
39 | saveHistory(history)
40 | }
41 |
42 | export function removeProjectFromHistory(project: ProjectHistory) {
43 | const history: ProjectHistory[] = getHistory()
44 |
45 | history.forEach((l, i) => {
46 | if (l.id === project.id) {
47 | history.splice(i, 1)
48 | }
49 | })
50 | saveHistory(history)
51 | }
52 |
--------------------------------------------------------------------------------
/src/pinia.ts:
--------------------------------------------------------------------------------
1 | import {createPinia} from 'pinia'
2 |
3 | const pinia = createPinia()
4 |
5 | export default pinia
6 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | // in order to use postcss-preset-env correctly we need some client side plugins
2 | import focusWithinInit from 'postcss-focus-within/browser'
3 | import cssHasPseudo from 'css-has-pseudo/browser'
4 |
5 | focusWithinInit()
6 | cssHasPseudo(document)
--------------------------------------------------------------------------------
/src/registerServiceWorker.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import {register} from 'register-service-worker'
4 |
5 | import {getFullBaseUrl} from './helpers/getFullBaseUrl'
6 |
7 | if (import.meta.env.PROD) {
8 | register(getFullBaseUrl() + 'sw.js', {
9 | ready() {
10 | console.log('App is being served from cache by a service worker.')
11 | },
12 | registered() {
13 | console.log('Service worker has been registered.')
14 | },
15 | cached() {
16 | console.log('Content has been cached for offline use.')
17 | },
18 | updatefound() {
19 | console.log('New content is downloading.')
20 | },
21 | updated(registration) {
22 | console.log('New content is available; please refresh.')
23 | // Send an event with the updated info
24 | document.dispatchEvent(
25 | new CustomEvent('swUpdated', {detail: registration}),
26 | )
27 | },
28 | offline() {
29 | console.log('No internet connection found. App is running in offline mode.')
30 | },
31 | error(error) {
32 | console.error('Error during service worker registration:', error)
33 | },
34 | })
35 | }
36 |
--------------------------------------------------------------------------------
/src/sentry.ts:
--------------------------------------------------------------------------------
1 | import 'virtual:vite-plugin-sentry/sentry-config'
2 | import type {App} from 'vue'
3 | import type {Router} from 'vue-router'
4 |
5 | export default async function setupSentry(app: App, router: Router) {
6 | const Sentry = await import('@sentry/vue')
7 | const {Integrations} = await import('@sentry/tracing')
8 |
9 | Sentry.init({
10 | app,
11 | dsn: window.SENTRY_DSN,
12 | release: import.meta.env.VITE_PLUGIN_SENTRY_CONFIG.release,
13 | dist: import.meta.env.VITE_PLUGIN_SENTRY_CONFIG.dist,
14 | integrations: [
15 | new Integrations.BrowserTracing({
16 | routingInstrumentation: Sentry.vueRouterInstrumentation(router),
17 | tracingOrigins: ['localhost', /^\//],
18 | }),
19 | ],
20 | tracesSampleRate: 1.0,
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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}/buckets',
10 | create: '/projects/{projectId}/buckets',
11 | update: '/projects/{projectId}/buckets/{id}',
12 | delete: '/projects/{projectId}/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/services/taskCollection.ts:
--------------------------------------------------------------------------------
1 | import AbstractService from '@/services/abstractService'
2 | import TaskModel from '@/models/task'
3 |
4 | import type {ITask} from '@/modelTypes/ITask'
5 |
6 | // FIXME: unite with other filter params types
7 | export interface GetAllTasksParams {
8 | sort_by: ('start_date' | 'done' | 'id')[],
9 | order_by: ('asc' | 'asc' | 'desc')[],
10 | filter_by: 'start_date'[],
11 | filter_comparator: ('greater_equals' | 'less_equals')[],
12 | filter_value: [string, string] // [dateFrom, dateTo],
13 | filter_concat: 'and',
14 | filter_include_nulls: boolean,
15 | }
16 |
17 | export default class TaskCollectionService extends AbstractService {
18 | constructor() {
19 | super({
20 | getAll: '/projects/{projectId}/tasks',
21 | })
22 | }
23 |
24 | modelFactory(data) {
25 | return new TaskModel(data)
26 | }
27 | }
--------------------------------------------------------------------------------
/src/services/taskComment.ts:
--------------------------------------------------------------------------------
1 | import AbstractService from './abstractService'
2 | import TaskCommentModel from '@/models/taskComment'
3 | import type {ITaskComment} from '@/modelTypes/ITaskComment'
4 |
5 | export default class TaskCommentService extends AbstractService {
6 | constructor() {
7 | super({
8 | create: '/tasks/{taskId}/comments',
9 | getAll: '/tasks/{taskId}/comments',
10 | get: '/tasks/{taskId}/comments/{id}',
11 | update: '/tasks/{taskId}/comments/{id}',
12 | delete: '/tasks/{taskId}/comments/{id}',
13 | })
14 | }
15 |
16 | modelFactory(data) {
17 | return new TaskCommentModel(data)
18 | }
19 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/stores/attachments.ts:
--------------------------------------------------------------------------------
1 | import {ref, readonly} from 'vue'
2 | import {defineStore, acceptHMRUpdate} from 'pinia'
3 | import {findIndexById} from '@/helpers/utils'
4 |
5 | import type {IAttachment} from '@/modelTypes/IAttachment'
6 |
7 | export const useAttachmentStore = defineStore('attachment', () => {
8 | const attachments = ref([])
9 |
10 | function set(newAttachments: IAttachment[]) {
11 | console.debug('Set attachments', newAttachments)
12 | attachments.value = newAttachments
13 | }
14 |
15 | function add(attachment: IAttachment) {
16 | console.debug('Add attachement', attachment)
17 | attachments.value.push(attachment)
18 | }
19 |
20 | function removeById(id: IAttachment['id']) {
21 | const attachmentIndex = findIndexById(attachments.value, id)
22 | attachments.value.splice(attachmentIndex, 1)
23 | console.debug('Remove attachement', id)
24 | }
25 |
26 | return {
27 | attachments: readonly(attachments),
28 | set,
29 | add,
30 | removeById,
31 | }
32 | })
33 |
34 | // support hot reloading
35 | if (import.meta.hot) {
36 | import.meta.hot.accept(acceptHMRUpdate(useAttachmentStore, import.meta.hot))
37 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/styles/common-imports.scss:
--------------------------------------------------------------------------------
1 | //
2 | // IMPORTANT NOTE:
3 | //
4 | // The styles in this file and all imported styles should not
5 | // create CSS output when compiled!
6 | // Instead they should only define SCSS that gets compiled to nothing.
7 | //
8 | // The reason is that this file is prefixed in _every_ component style so that
9 | // the component has access to the variables, mixins, etc. that
10 | // are defined here.
11 | //
12 |
13 | $family-sans-serif: 'Open Sans', Helvetica, Arial, sans-serif;
14 |
15 | // the default values get overwritten by the definitions above
16 | @import "bulma-css-variables/sass/utilities/_all";
17 |
18 | // since $tablet is defined by bulma we can just define it after importing the utilities
19 | $mobile: math.div($tablet, 2);
20 |
21 | $vikunja-font: 'Quicksand', sans-serif;
22 |
23 | $pagination-current-border: var(--primary);
24 | $navbar-item-active-color: var(--primary);
25 |
26 | $site-background: var(--grey-100);
27 |
28 | $transition-duration: 150ms;
29 | $transition: $transition-duration ease;
30 |
31 | $button-height: 34px;
32 | $switch-view-height: 2.69rem;
33 |
34 | $navbar-height: 4rem;
35 | $navbar-width: 300px;
36 | $navbar-padding: 2rem;
37 |
38 | $vikunja-nav-color: var(--grey-700);
39 | $vikunja-nav-selected-width: 0.4rem;
40 |
41 | $close-button-min-space: 84px;
42 |
--------------------------------------------------------------------------------
/src/styles/components/_index.scss:
--------------------------------------------------------------------------------
1 | @import "tooltip";
2 | @import "labels";
3 | @import "project";
4 | @import "task";
5 | @import "tasks";
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/styles/custom-properties/_index.scss:
--------------------------------------------------------------------------------
1 | @import "colors";
2 | @import "shadows";
--------------------------------------------------------------------------------
/src/styles/custom-properties/shadows.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --shadow-xs: 0 1px 3px hsla(var(--grey-500-hsl), .12),
3 | 0 1px 2px hsla(var(--grey-500-hsl), .24);
4 | --shadow-sm: 0 3px 6px hsla(var(--grey-500-hsl), .12),
5 | 0 2px 4px hsla(var(--grey-500-hsl), .10);
6 | --shadow-md: 0 10px 20px hsla(var(--grey-500-hsl), .12),
7 | 0 3px 6px hsla(var(--grey-500-hsl), .08);
8 | --shadow-lg: 0 15px 25px hsla(var(--grey-500-hsl), .12),
9 | 0 5px 10px hsla(var(--grey-500-hsl), .05);
10 |
11 | &.dark {
12 | // Even darker shadows for dark mode
13 | --shadow-xs: 0 1px 3px hsla(var(--grey-50-hsl), 0.4),
14 | 0 1px 2px hsla(var(--grey-50-hsl), 0.8);
15 | --shadow-sm: 0 3px 6px hsla(var(--grey-50-hsl), 0.8),
16 | 0 2px 4px hsla(var(--grey-50-hsl), 0.6);
17 | --shadow-md: 0 10px 20px hsla(var(--grey-50-hsl), 0.8),
18 | 0 3px 6px hsla(var(--grey-50-hsl), 0.6);
19 | --shadow-lg: 0 15px 25px hsla(var(--grey-50-hsl), 0.8),
20 | 0 5px 10px hsla(var(--grey-50-hsl), 0.4);
21 | }
22 | }
--------------------------------------------------------------------------------
/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';
--------------------------------------------------------------------------------
/src/styles/theme/background.scss:
--------------------------------------------------------------------------------
1 | .app-container.has-background,
2 | .link-share-container.has-background {
3 | position: relative;
4 |
5 | &, .app-container-background {
6 | background-position: center;
7 | background-size: cover;
8 | background-repeat: no-repeat;
9 | background-attachment: fixed;
10 | min-height: 100vh;
11 | }
12 |
13 | // FIXME: move to pagination component
14 | .pagination-link:not(.is-current) {
15 | background: var(--grey-100);
16 | }
17 |
18 | .box,
19 | .card,
20 | .switch-view,
21 | .project-table .button,
22 | .filter-container .button,
23 | .search .button {
24 | box-shadow: none;
25 | }
26 |
27 | .task-view {
28 | border-radius: $radius;
29 |
30 | @media screen and (min-width: $tablet) {
31 | margin-inline: 1rem;
32 | }
33 | }
34 |
35 | .kanban .tasks {
36 | background: transparent;
37 |
38 | .task {
39 | border-radius: $radius !important;
40 | }
41 | }
42 | }
43 |
44 | .app-container-background {
45 | width: 100vw;
46 | height: 100vh;
47 | position: fixed;
48 | z-index: 0;
49 | }
50 |
51 | .background-fade-in {
52 | opacity: 0;
53 | transition: opacity $transition;
54 | transition-delay: $transition-duration * 2; // To fake an appearing background
55 |
56 | &.is-visible {
57 | opacity: 1;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/styles/theme/helpers.scss:
--------------------------------------------------------------------------------
1 | .d-print-none {
2 | @media print {
3 | display: none !important;
4 | }
5 | }
--------------------------------------------------------------------------------
/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 | }
18 |
19 | .link-share-container {
20 | &.project\.gantt-view,
21 | &.project\.kanban-view {
22 | .container {
23 | max-width: 100vw;
24 |
25 | .column {
26 | width: 100%;
27 | margin: 0;
28 | }
29 | }
30 | }
31 | }
32 |
33 | .link-share-container:not(.has-background) {
34 | .list-view {
35 | max-width: 100%;
36 | }
37 |
38 | .loader-container, .gantt-chart-container > .card {
39 | box-shadow: none !important;
40 | border: none;
41 |
42 | .task-add {
43 | padding: 1rem 0 0;
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
7 | new Date().toISOString()
--------------------------------------------------------------------------------
/src/types/DateKebab.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Date in Format 2022-12-10
3 | */
4 | export type DateKebab = `${string}-${string}-${string}`
5 |
--------------------------------------------------------------------------------
/src/types/IFilter.ts:
--------------------------------------------------------------------------------
1 | export interface IFilter {
2 | sortBy: ('done' | 'id')[]
3 | orderBy: ('asc' | 'desc')[]
4 | filterBy: 'done'[]
5 | filterValue: 'false'[]
6 | filterComparator: 'equals'[]
7 | filterConcat: 'and'
8 | filterIncludeNulls: boolean
9 | }
--------------------------------------------------------------------------------
/src/types/IProvider.ts:
--------------------------------------------------------------------------------
1 | export interface IProvider {
2 | name: string;
3 | key: string;
4 | authUrl: string;
5 | clientId: string;
6 | logoutUrl: string;
7 | }
8 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/types/PartialWithId.ts:
--------------------------------------------------------------------------------
1 | export type PartialWithId = Pick & Omit, 'id'>
--------------------------------------------------------------------------------
/src/types/ProjectView.ts:
--------------------------------------------------------------------------------
1 | export const PROJECT_VIEWS = {
2 | LIST: 'list',
3 | GANTT: 'gantt',
4 | TABLE: 'table',
5 | KANBAN: 'kanban',
6 | } as const
7 |
8 | export type ProjectView = typeof PROJECT_VIEWS[keyof typeof PROJECT_VIEWS]
9 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/types/global-components.d.ts:
--------------------------------------------------------------------------------
1 | // import FontAwesomeIcon from '@/components/misc/Icon'
2 | import type { FontAwesomeIcon as FontAwesomeIconFixedTypes } from './vue-fontawesome'
3 | import type XButton from '@/components/input/button.vue'
4 | import type Modal from '@/components/misc/modal.vue'
5 | import type Card from '@/components/misc/card.vue'
6 |
7 | // Here we define globally imported components
8 | // See:
9 | // https://github.com/johnsoncodehk/volar/blob/2ca8fd3434423c7bea1c8e08132df3b9ce84eea7/extensions/vscode-vue-language-features/README.md#usage
10 | // Under the hidden collapsible "Define Global Components"
11 |
12 | declare module '@vue/runtime-core' {
13 | export interface GlobalComponents {
14 | Icon: FontAwesomeIconFixedTypes
15 | XButton: typeof XButton,
16 | Modal: typeof Modal,
17 | Card: typeof Card,
18 | }
19 | }
20 |
21 | export {}
22 |
--------------------------------------------------------------------------------
/src/types/shims-tsx.d.ts:
--------------------------------------------------------------------------------
1 | import Vue, { VNode } from 'vue'
2 |
3 | declare global {
4 | namespace JSX {
5 | // tslint:disable no-empty-interface
6 | interface Element extends VNode {}
7 | // tslint:disable no-empty-interface
8 | interface ElementClass extends Vue {}
9 | interface IntrinsicElements {
10 | [elem: string]: any
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/types/vue-fontawesome.ts:
--------------------------------------------------------------------------------
1 | // copied and slightly modified from unmerged pull request that corrects types
2 | // https://github.com/FortAwesome/vue-fontawesome/pull/355
3 |
4 | import type { FaSymbol, FlipProp, IconLookup, IconProp, PullProp, SizeProp, Transform } from '@fortawesome/fontawesome-svg-core'
5 | import type { DefineComponent } from 'vue'
6 |
7 |
8 | interface FontAwesomeIconProps {
9 | border?: boolean
10 | fixedWidth?: boolean
11 | flip?: FlipProp
12 | icon: IconProp
13 | mask?: IconLookup
14 | listItem?: boolean
15 | pull?: PullProp
16 | pulse?: boolean
17 | rotation?: 90 | 180 | 270 | '90' | '180' | '270'
18 | swapOpacity?: boolean
19 | size?: SizeProp
20 | spin?: boolean
21 | transform?: Transform
22 | symbol?: FaSymbol
23 | title?: string | string[]
24 | inverse?: boolean
25 | }
26 |
27 | interface FontAwesomeLayersProps {
28 | fixedWidth?: boolean
29 | }
30 |
31 | interface FontAwesomeLayersTextProps {
32 | value: string | number
33 | transform?: object | string
34 | counter?: boolean
35 | position?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right'
36 | }
37 |
38 | export type FontAwesomeIcon = DefineComponent
39 | export type FontAwesomeLayers = DefineComponent
40 | export type FontAwesomeLayersText = DefineComponent
--------------------------------------------------------------------------------
/src/urls.ts:
--------------------------------------------------------------------------------
1 | export const POWERED_BY = 'https://vikunja.io'
2 | export const CALDAV_DOCS = 'https://vikunja.io/docs/caldav/'
3 |
--------------------------------------------------------------------------------
/src/version.json:
--------------------------------------------------------------------------------
1 | {
2 | "VERSION": "dev"
3 | }
--------------------------------------------------------------------------------
/src/views/404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ $t('404.title') }}
4 |
{{ $t('404.text') }}
5 |
6 |
7 |
8 |
13 |
--------------------------------------------------------------------------------
/src/views/About.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
14 |
15 |
16 | {{ $t('about.frontendVersion', {version: frontendVersion}) }}
17 |
18 |
19 | {{ $t('about.apiVersion', {version: apiVersion}) }}
20 |
21 |
22 |
23 |
27 | {{ $t('misc.close') }}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
44 |
--------------------------------------------------------------------------------
/src/views/filters/FilterDelete.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | {{ $t('filters.delete.header') }}
8 |
9 |
10 |
11 | {{ $t('filters.delete.text') }}
12 |
13 |
14 |
15 |
16 |
28 |
--------------------------------------------------------------------------------
/src/views/migrate/icons/ticktick.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/views/migrate/icons/todoist.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/views/migrate/icons/trello.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/views/migrate/icons/vikunja-file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/src/views/migrate/icons/vikunja-file.png
--------------------------------------------------------------------------------
/src/views/migrate/icons/wunderlist.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-vikunja/frontend/652d3c738438b5fd170cc941bdedae916f99ba09/src/views/migrate/icons/wunderlist.jpg
--------------------------------------------------------------------------------
/src/views/migrate/migrators.ts:
--------------------------------------------------------------------------------
1 | import wunderlistIcon from './icons/wunderlist.jpg'
2 | import todoistIcon from './icons/todoist.svg?url'
3 | import trelloIcon from './icons/trello.svg?url'
4 | import microsoftTodoIcon from './icons/microsoft-todo.svg?url'
5 | import vikunjaFileIcon from './icons/vikunja-file.png?url'
6 | import tickTickIcon from './icons/ticktick.svg?url'
7 |
8 | export interface Migrator {
9 | id: string
10 | name: string
11 | isFileMigrator?: boolean
12 | icon: string
13 | }
14 |
15 | interface IMigratorRecord {
16 | [key: Migrator['id']]: Migrator
17 | }
18 |
19 | export const MIGRATORS = {
20 | wunderlist: {
21 | id: 'wunderlist',
22 | name: 'Wunderlist',
23 | icon: wunderlistIcon,
24 | },
25 | todoist: {
26 | id: 'todoist',
27 | name: 'Todoist',
28 | icon: todoistIcon as string,
29 | },
30 | trello: {
31 | id: 'trello',
32 | name: 'Trello',
33 | icon: trelloIcon as string,
34 | },
35 | 'microsoft-todo': {
36 | id: 'microsoft-todo',
37 | name: 'Microsoft Todo',
38 | icon: microsoftTodoIcon as string,
39 | },
40 | 'vikunja-file': {
41 | id: 'vikunja-file',
42 | name: 'Vikunja Export',
43 | icon: vikunjaFileIcon,
44 | isFileMigrator: true,
45 | },
46 | ticktick: {
47 | id: 'ticktick',
48 | name: 'TickTick',
49 | icon: tickTickIcon as string,
50 | isFileMigrator: true,
51 | },
52 | } as const satisfies IMigratorRecord
53 |
--------------------------------------------------------------------------------
/src/views/project/ProjectInfo.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
13 |
17 | {{ $t('project.noDescriptionAvailable') }}
18 |
19 |
20 |
21 |
22 |
23 |
46 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.dom.json",
3 | "include": ["env.d.ts", "src/**/*.d.ts", "src/**/*", "src/**/*.vue", "src/**/*.json"],
4 | "exclude": ["src/**/__tests__/*"],
5 | "compilerOptions": {
6 | "composite": true,
7 | "baseUrl": ".",
8 | "lib": ["ESNext", "DOM", "WebWorker"],
9 |
10 | "importHelpers": true,
11 | "sourceMap": true,
12 | "strictNullChecks": true,
13 |
14 | "paths": {
15 | "@/*": ["./src/*"]
16 | },
17 | "types": [
18 | // https://github.com/ikenfin/vite-plugin-sentry#typescript
19 | "vite-plugin-sentry/client"
20 | ],
21 | "ignoreDeprecations": "5.0"
22 | },
23 | "vueCompilerOptions": {
24 | // "strictTemplates": true
25 | "jsxTemplates": true
26 | }
27 | }
--------------------------------------------------------------------------------
/tsconfig.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "@tsconfig/node18/tsconfig.json",
4 | "@vue/tsconfig/tsconfig.json"
5 | ],
6 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "env.config.d.ts"],
7 | "compilerOptions": {
8 | "composite": true,
9 | "types": ["node"]
10 | }
11 | }
--------------------------------------------------------------------------------
/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 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.vitest.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.app.json",
3 | "exclude": [],
4 | "compilerOptions": {
5 | "composite": true,
6 | "lib": [],
7 | "types": ["node"]
8 | }
9 | }
--------------------------------------------------------------------------------