Compare commits
12 Commits
3b9d1d128d
...
d51f340f7b
Author | SHA1 | Date |
---|---|---|
renovate | d51f340f7b | |
renovate | e19ac57130 | |
kolaente | 0557d4b5bb | |
kolaente | bc19a2fb78 | |
kolaente | 994aaeb920 | |
kolaente | ee3d20e1d2 | |
Elscrux | 8458e77341 | |
kolaente | af3b0bbea1 | |
renovate | d2317b9531 | |
renovate | 552f8580a4 | |
renovate | c842cb27b2 | |
kolaente | e10cd368bf |
|
@ -5,7 +5,7 @@
|
|||
"eslint.packageManager": "pnpm",
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
"eslint.format.enable": true,
|
||||
"[javascript]": {
|
||||
|
|
|
@ -10,7 +10,7 @@ menu:
|
|||
|
||||
# Vikunja Versions
|
||||
|
||||
The Vikunja api and frontend are available in two different release flavors.
|
||||
Vikunja api is available in two different release flavors.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
|
@ -30,6 +30,9 @@ There might be multiple new such builds a day.
|
|||
Versions contain the last stable version, the number of commits since then and the commit the currently running binary was built from.
|
||||
They look like this: `v0.18.1+269-5cc4927b9e`
|
||||
|
||||
Since a release is also cut from the main branch at some point, features from unstable will eventually become available in stable releases.
|
||||
At the point in time of a new version release, the unstable build is the same exact thing.
|
||||
|
||||
The demo instance at [try.vikunja.io](https://try.vikunja.io) automatically updates and always runs the last unstable build.
|
||||
|
||||
## Switching between versions
|
||||
|
|
|
@ -1 +1 @@
|
|||
20.12.1
|
||||
20.12.2
|
|
@ -60,39 +60,39 @@
|
|||
"@kyvg/vue3-notification": "3.2.1",
|
||||
"@sentry/tracing": "7.109.0",
|
||||
"@sentry/vue": "7.109.0",
|
||||
"@tiptap/core": "2.2.4",
|
||||
"@tiptap/extension-blockquote": "2.2.4",
|
||||
"@tiptap/extension-bold": "2.2.4",
|
||||
"@tiptap/extension-bullet-list": "2.2.4",
|
||||
"@tiptap/extension-code": "2.2.4",
|
||||
"@tiptap/extension-code-block-lowlight": "2.2.4",
|
||||
"@tiptap/extension-document": "2.2.4",
|
||||
"@tiptap/extension-dropcursor": "2.2.4",
|
||||
"@tiptap/extension-gapcursor": "2.2.4",
|
||||
"@tiptap/extension-hard-break": "2.2.4",
|
||||
"@tiptap/extension-heading": "2.2.4",
|
||||
"@tiptap/extension-history": "2.2.4",
|
||||
"@tiptap/extension-horizontal-rule": "2.2.4",
|
||||
"@tiptap/extension-image": "2.2.4",
|
||||
"@tiptap/extension-italic": "2.2.4",
|
||||
"@tiptap/extension-link": "2.2.4",
|
||||
"@tiptap/extension-list-item": "2.2.4",
|
||||
"@tiptap/extension-ordered-list": "2.2.4",
|
||||
"@tiptap/extension-paragraph": "2.2.4",
|
||||
"@tiptap/extension-placeholder": "2.2.4",
|
||||
"@tiptap/extension-strike": "2.2.4",
|
||||
"@tiptap/extension-table": "2.2.4",
|
||||
"@tiptap/extension-table-cell": "2.2.4",
|
||||
"@tiptap/extension-table-header": "2.2.4",
|
||||
"@tiptap/extension-table-row": "2.2.4",
|
||||
"@tiptap/extension-task-item": "2.2.4",
|
||||
"@tiptap/extension-task-list": "2.2.4",
|
||||
"@tiptap/extension-text": "2.2.4",
|
||||
"@tiptap/extension-typography": "2.2.4",
|
||||
"@tiptap/extension-underline": "2.2.4",
|
||||
"@tiptap/pm": "2.2.4",
|
||||
"@tiptap/suggestion": "2.2.4",
|
||||
"@tiptap/vue-3": "2.2.4",
|
||||
"@tiptap/core": "2.2.6",
|
||||
"@tiptap/extension-blockquote": "2.2.6",
|
||||
"@tiptap/extension-bold": "2.2.6",
|
||||
"@tiptap/extension-bullet-list": "2.2.6",
|
||||
"@tiptap/extension-code": "2.2.6",
|
||||
"@tiptap/extension-code-block-lowlight": "2.2.6",
|
||||
"@tiptap/extension-document": "2.2.6",
|
||||
"@tiptap/extension-dropcursor": "2.2.6",
|
||||
"@tiptap/extension-gapcursor": "2.2.6",
|
||||
"@tiptap/extension-hard-break": "2.2.6",
|
||||
"@tiptap/extension-heading": "2.2.6",
|
||||
"@tiptap/extension-history": "2.2.6",
|
||||
"@tiptap/extension-horizontal-rule": "2.2.6",
|
||||
"@tiptap/extension-image": "2.2.6",
|
||||
"@tiptap/extension-italic": "2.2.6",
|
||||
"@tiptap/extension-link": "2.2.6",
|
||||
"@tiptap/extension-list-item": "2.2.6",
|
||||
"@tiptap/extension-ordered-list": "2.2.6",
|
||||
"@tiptap/extension-paragraph": "2.2.6",
|
||||
"@tiptap/extension-placeholder": "2.2.6",
|
||||
"@tiptap/extension-strike": "2.2.6",
|
||||
"@tiptap/extension-table": "2.2.6",
|
||||
"@tiptap/extension-table-cell": "2.2.6",
|
||||
"@tiptap/extension-table-header": "2.2.6",
|
||||
"@tiptap/extension-table-row": "2.2.6",
|
||||
"@tiptap/extension-task-item": "2.2.6",
|
||||
"@tiptap/extension-task-list": "2.2.6",
|
||||
"@tiptap/extension-text": "2.2.6",
|
||||
"@tiptap/extension-typography": "2.2.6",
|
||||
"@tiptap/extension-underline": "2.2.6",
|
||||
"@tiptap/pm": "2.2.6",
|
||||
"@tiptap/suggestion": "2.2.6",
|
||||
"@tiptap/vue-3": "2.2.6",
|
||||
"@types/is-touch-device": "1.0.2",
|
||||
"@types/lodash.clonedeep": "4.5.9",
|
||||
"@vueuse/core": "10.9.0",
|
||||
|
@ -103,7 +103,7 @@
|
|||
"camel-case": "4.1.2",
|
||||
"date-fns": "3.6.0",
|
||||
"dayjs": "1.11.10",
|
||||
"dompurify": "3.0.11",
|
||||
"dompurify": "3.1.0",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"flatpickr": "4.6.13",
|
||||
"flexsearch": "0.7.31",
|
||||
|
@ -121,7 +121,7 @@
|
|||
"vue": "3.4.21",
|
||||
"vue-advanced-cropper": "2.8.8",
|
||||
"vue-flatpickr-component": "11.0.5",
|
||||
"vue-i18n": "9.10.2",
|
||||
"vue-i18n": "9.11.0",
|
||||
"vue-router": "4.3.0",
|
||||
"vuemoji-picker": "0.2.1",
|
||||
"workbox-precaching": "7.0.0",
|
||||
|
@ -132,8 +132,8 @@
|
|||
"@cypress/vite-dev-server": "5.0.7",
|
||||
"@cypress/vue": "6.0.0",
|
||||
"@faker-js/faker": "8.4.1",
|
||||
"@histoire/plugin-screenshot": "0.17.15",
|
||||
"@histoire/plugin-vue": "0.17.15",
|
||||
"@histoire/plugin-screenshot": "0.17.17",
|
||||
"@histoire/plugin-vue": "0.17.17",
|
||||
"@rushstack/eslint-patch": "1.10.1",
|
||||
"@tsconfig/node18": "18.2.4",
|
||||
"@types/codemirror": "5.60.15",
|
||||
|
@ -142,11 +142,11 @@
|
|||
"@types/is-touch-device": "1.0.2",
|
||||
"@types/lodash.debounce": "4.0.9",
|
||||
"@types/marked": "5.0.2",
|
||||
"@types/node": "20.12.5",
|
||||
"@types/node": "20.12.7",
|
||||
"@types/postcss-preset-env": "7.7.0",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "7.5.0",
|
||||
"@typescript-eslint/parser": "7.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.6.0",
|
||||
"@typescript-eslint/parser": "7.6.0",
|
||||
"@vitejs/plugin-legacy": "5.3.2",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"@vue/eslint-config-typescript": "13.0.0",
|
||||
|
@ -154,15 +154,15 @@
|
|||
"@vue/tsconfig": "0.5.1",
|
||||
"autoprefixer": "10.4.19",
|
||||
"browserslist": "4.23.0",
|
||||
"caniuse-lite": "1.0.30001607",
|
||||
"caniuse-lite": "1.0.30001608",
|
||||
"css-has-pseudo": "6.0.3",
|
||||
"csstype": "3.1.3",
|
||||
"cypress": "13.7.2",
|
||||
"esbuild": "0.20.2",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-vue": "9.24.0",
|
||||
"eslint-plugin-vue": "9.24.1",
|
||||
"happy-dom": "14.7.1",
|
||||
"histoire": "0.17.15",
|
||||
"histoire": "0.17.17",
|
||||
"postcss": "8.4.38",
|
||||
"postcss-easing-gradients": "3.0.1",
|
||||
"postcss-easings": "4.0.0",
|
||||
|
@ -172,14 +172,14 @@
|
|||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"sass": "1.74.1",
|
||||
"start-server-and-test": "2.0.3",
|
||||
"typescript": "5.4.4",
|
||||
"typescript": "5.4.5",
|
||||
"vite": "5.2.8",
|
||||
"vite-plugin-inject-preload": "1.3.3",
|
||||
"vite-plugin-pwa": "0.19.8",
|
||||
"vite-plugin-sentry": "1.4.0",
|
||||
"vite-svg-loader": "5.1.0",
|
||||
"vitest": "1.4.0",
|
||||
"vue-tsc": "2.0.11",
|
||||
"vue-tsc": "2.0.12",
|
||||
"wait-on": "7.2.0",
|
||||
"workbox-cli": "7.0.0"
|
||||
},
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -155,7 +155,6 @@ const savedFilterProjects = computed(() => projectStore.savedFilterProjects)
|
|||
bottom: 0;
|
||||
left: 0;
|
||||
transform: translateX(-100%);
|
||||
overflow-x: auto;
|
||||
width: $navbar-width;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
|
|
|
@ -18,19 +18,32 @@ package handler
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/modules/migration"
|
||||
"code.vikunja.io/api/pkg/notifications"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill/message"
|
||||
"github.com/getsentry/sentry-go"
|
||||
)
|
||||
|
||||
func RegisterListeners() {
|
||||
events.RegisterListener((&MigrationRequestedEvent{}).Name(), &MigrationListener{})
|
||||
}
|
||||
|
||||
// Only used for sentry
|
||||
type migrationFailedError struct {
|
||||
MigratorKind string
|
||||
OriginalError error
|
||||
}
|
||||
|
||||
func (m *migrationFailedError) Error() string {
|
||||
return fmt.Sprintf("migration from %s failed, original error message was: %s", m.MigratorKind, m.OriginalError.Error())
|
||||
}
|
||||
|
||||
// MigrationListener represents a listener
|
||||
type MigrationListener struct {
|
||||
}
|
||||
|
@ -59,13 +72,46 @@ func (s *MigrationListener) Handle(msg *message.Message) (err error) {
|
|||
|
||||
ms := event.Migrator.(migration.Migrator)
|
||||
|
||||
m, err := migration.StartMigration(ms, event.User)
|
||||
m, err := migrateInListener(ms, event)
|
||||
if err != nil {
|
||||
log.Errorf("[Migration] Migration %d from %s for user %d failed. Error was: %s", m.ID, event.MigratorKind, event.User.ID, err.Error())
|
||||
|
||||
var nerr error
|
||||
if config.SentryEnabled.GetBool() {
|
||||
nerr = notifications.Notify(event.User, &MigrationFailedReportedNotification{
|
||||
MigratorName: ms.Name(),
|
||||
})
|
||||
sentry.CaptureException(&migrationFailedError{
|
||||
MigratorKind: event.MigratorKind,
|
||||
OriginalError: err,
|
||||
})
|
||||
} else {
|
||||
nerr = notifications.Notify(event.User, &MigrationFailedNotification{
|
||||
MigratorName: ms.Name(),
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
if nerr != nil {
|
||||
log.Errorf("[Migration] Could not sent failed migration notification for migration %d to user %d, error was: %s", m.ID, event.User.ID, err.Error())
|
||||
}
|
||||
|
||||
// Still need to finish the migration, otherwise restarting will not work
|
||||
err = migration.FinishMigration(m)
|
||||
if err != nil {
|
||||
log.Errorf("[Migration] Could not finish migration %d for user %d, error was: %s", m.ID, event.User.ID, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil // We do not want the queue to restart this job as we've already handled the error.
|
||||
}
|
||||
|
||||
func migrateInListener(ms migration.Migrator, event *MigrationRequestedEvent) (m *migration.Status, err error) {
|
||||
m, err = migration.StartMigration(ms, event.User)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("[Migration] Starting migration %d from %s for user %d", m.ID, event.MigratorKind, event.User.ID)
|
||||
|
||||
err = ms.Migrate(event.User)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -73,6 +119,7 @@ func (s *MigrationListener) Handle(msg *message.Message) (err error) {
|
|||
|
||||
err = migration.FinishMigration(m)
|
||||
if err != nil {
|
||||
log.Errorf("[Migration] Could not finish migration %d for user %d, error was: %s", m.ID, event.User.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -80,6 +127,7 @@ func (s *MigrationListener) Handle(msg *message.Message) (err error) {
|
|||
MigratorName: ms.Name(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("[Migration] Could not sent migration success notification for migration %d to user %d, error was: %s", m.ID, event.User.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -49,3 +49,57 @@ func (n *MigrationDoneNotification) ToDB() interface{} {
|
|||
func (n *MigrationDoneNotification) Name() string {
|
||||
return "migration.done"
|
||||
}
|
||||
|
||||
// MigrationFailedReportedNotification represents a MigrationFailedReportedNotification notification
|
||||
type MigrationFailedReportedNotification struct {
|
||||
MigratorName string
|
||||
}
|
||||
|
||||
// ToMail returns the mail notification for MigrationFailedReportedNotification
|
||||
func (n *MigrationFailedReportedNotification) ToMail() *notifications.Mail {
|
||||
kind := cases.Title(language.English).String(n.MigratorName)
|
||||
|
||||
return notifications.NewMail().
|
||||
Subject("The migration from " + kind + " to Vikunja was has failed").
|
||||
Line("Looks like the move from " + kind + " didn't go as planned this time.").
|
||||
Line("No worries, though! Just give it another shot by starting over the same way you did before. Sometimes, these hiccups happen because of glitches on " + kind + "'s end, but trying again often does the trick.").
|
||||
Line("We've got the error message on our radar and are on it to get it sorted out soon.")
|
||||
}
|
||||
|
||||
// ToDB returns the MigrationFailedReportedNotification notification in a format which can be saved in the db
|
||||
func (n *MigrationFailedReportedNotification) ToDB() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the name of the notification
|
||||
func (n *MigrationFailedReportedNotification) Name() string {
|
||||
return "migration.failed.reported"
|
||||
}
|
||||
|
||||
// MigrationFailedNotification represents a MigrationFailedNotification notification
|
||||
type MigrationFailedNotification struct {
|
||||
MigratorName string
|
||||
Error error
|
||||
}
|
||||
|
||||
// ToMail returns the mail notification for MigrationFailedNotification
|
||||
func (n *MigrationFailedNotification) ToMail() *notifications.Mail {
|
||||
kind := cases.Title(language.English).String(n.MigratorName)
|
||||
|
||||
return notifications.NewMail().
|
||||
Subject("The migration from " + kind + " to Vikunja was has failed").
|
||||
Line("Looks like the move from " + kind + " didn't go as planned this time.").
|
||||
Line("No worries, though! Just give it another shot by starting over the same way you did before. Sometimes, these hiccups happen because of glitches on " + kind + "'s end, but trying again often does the trick.").
|
||||
Line("We bumped into a little error along the way: `" + n.Error.Error() + "`.").
|
||||
Line("Please drop us a note about this [in the forum](https://community.vikunja.io/) or any of the usual places so that we can take a look at why it failed.")
|
||||
}
|
||||
|
||||
// ToDB returns the MigrationFailedNotification notification in a format which can be saved in the db
|
||||
func (n *MigrationFailedNotification) ToDB() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the name of the notification
|
||||
func (n *MigrationFailedNotification) Name() string {
|
||||
return "migration.failed"
|
||||
}
|
||||
|
|
|
@ -107,80 +107,109 @@ func (m *Migration) AuthURL() string {
|
|||
"&return_url=" + config.MigrationTrelloRedirectURL.GetString()
|
||||
}
|
||||
|
||||
func getTrelloData(token string) (trelloData []*trello.Board, err error) {
|
||||
allArg := trello.Arguments{"fields": "all"}
|
||||
|
||||
client := trello.NewClient(config.MigrationTrelloKey.GetString(), token)
|
||||
client.Logger = log.GetLogger()
|
||||
|
||||
func getTrelloBoards(client *trello.Client) (trelloData []*trello.Board, err error) {
|
||||
log.Debugf("[Trello Migration] Getting boards...")
|
||||
|
||||
trelloData, err = client.GetMyBoards(trello.Defaults())
|
||||
if err != nil {
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("[Trello Migration] Got %d trello boards", len(trelloData))
|
||||
|
||||
for _, board := range trelloData {
|
||||
log.Debugf("[Trello Migration] Getting projects for board %s", board.ID)
|
||||
return
|
||||
}
|
||||
|
||||
board.Lists, err = board.GetLists(trello.Defaults())
|
||||
if err != nil {
|
||||
return
|
||||
func getTrelloOrganizationsWithBoards(boards []*trello.Board) (boardsByOrg map[string][]*trello.Board) {
|
||||
|
||||
boardsByOrg = make(map[string][]*trello.Board)
|
||||
|
||||
for _, board := range boards {
|
||||
// Trello boards without an organization are considered personal boards
|
||||
if board.IDOrganization == "" {
|
||||
board.IDOrganization = "Personal"
|
||||
}
|
||||
|
||||
log.Debugf("[Trello Migration] Got %d projects for board %s", len(board.Lists), board.ID)
|
||||
|
||||
listMap := make(map[string]*trello.List, len(board.Lists))
|
||||
for _, list := range board.Lists {
|
||||
listMap[list.ID] = list
|
||||
_, has := boardsByOrg[board.IDOrganization]
|
||||
if !has {
|
||||
boardsByOrg[board.IDOrganization] = []*trello.Board{}
|
||||
}
|
||||
|
||||
log.Debugf("[Trello Migration] Getting cards for board %s", board.ID)
|
||||
boardsByOrg[board.IDOrganization] = append(boardsByOrg[board.IDOrganization], board)
|
||||
}
|
||||
|
||||
cards, err := board.GetCards(allArg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return
|
||||
}
|
||||
|
||||
func fillCardData(client *trello.Client, board *trello.Board) (err error) {
|
||||
allArg := trello.Arguments{"fields": "all"}
|
||||
|
||||
log.Debugf("[Trello Migration] Getting projects for board %s", board.ID)
|
||||
|
||||
board.Lists, err = board.GetLists(trello.Defaults())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("[Trello Migration] Got %d projects for board %s", len(board.Lists), board.ID)
|
||||
|
||||
listMap := make(map[string]*trello.List, len(board.Lists))
|
||||
for _, list := range board.Lists {
|
||||
listMap[list.ID] = list
|
||||
}
|
||||
|
||||
log.Debugf("[Trello Migration] Getting cards for board %s", board.ID)
|
||||
|
||||
cards, err := board.GetCards(allArg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("[Trello Migration] Got %d cards for board %s", len(cards), board.ID)
|
||||
|
||||
for _, card := range cards {
|
||||
list, exists := listMap[card.IDList]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("[Trello Migration] Got %d cards for board %s", len(cards), board.ID)
|
||||
|
||||
for _, card := range cards {
|
||||
list, exists := listMap[card.IDList]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
if card.Badges.Attachments > 0 {
|
||||
card.Attachments, err = card.GetAttachments(allArg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return
|
||||
}
|
||||
|
||||
if len(card.IDCheckLists) > 0 {
|
||||
for _, checkListID := range card.IDCheckLists {
|
||||
checklist, err := client.GetChecklist(checkListID, allArg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
checklist.CheckItems = []trello.CheckItem{}
|
||||
err = client.Get("checklists/"+checkListID+"/checkItems", allArg, &checklist.CheckItems)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
card.Checklists = append(card.Checklists, checklist)
|
||||
log.Debugf("Retrieved checklist %s for card %s", checkListID, card.ID)
|
||||
}
|
||||
}
|
||||
|
||||
list.Cards = append(list.Cards, card)
|
||||
}
|
||||
|
||||
log.Debugf("[Trello Migration] Looked for attachements on all cards of board %s", board.ID)
|
||||
if card.Badges.Comments > 0 {
|
||||
card.Actions, err = card.GetCommentActions()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(card.IDCheckLists) > 0 {
|
||||
for _, checkListID := range card.IDCheckLists {
|
||||
checklist, err := client.GetChecklist(checkListID, allArg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checklist.CheckItems = []trello.CheckItem{}
|
||||
err = client.Get("checklists/"+checkListID+"/checkItems", allArg, &checklist.CheckItems)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
card.Checklists = append(card.Checklists, checklist)
|
||||
log.Debugf("Retrieved checklist %s for card %s", checkListID, card.ID)
|
||||
}
|
||||
}
|
||||
|
||||
list.Cards = append(list.Cards, card)
|
||||
}
|
||||
|
||||
log.Debugf("[Trello Migration] Looked for attachements on all cards of board %s", board.ID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -196,7 +225,7 @@ func convertMarkdownToHTML(input string) (output string, err error) {
|
|||
|
||||
// Converts all previously obtained data from trello into the vikunja format.
|
||||
// `trelloData` should contain all boards with their projects and cards respectively.
|
||||
func convertTrelloDataToVikunja(trelloData []*trello.Board, token string) (fullVikunjaHierachie []*models.ProjectWithTasksAndBuckets, err error) {
|
||||
func convertTrelloDataToVikunja(organizationName string, trelloData []*trello.Board, token string, currentMember *trello.Member) (fullVikunjaHierachie []*models.ProjectWithTasksAndBuckets, err error) {
|
||||
|
||||
log.Debugf("[Trello Migration] ")
|
||||
|
||||
|
@ -205,7 +234,7 @@ func convertTrelloDataToVikunja(trelloData []*trello.Board, token string) (fullV
|
|||
{
|
||||
Project: models.Project{
|
||||
ID: pseudoParentID,
|
||||
Title: "Imported from Trello",
|
||||
Title: organizationName,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -252,9 +281,11 @@ func convertTrelloDataToVikunja(trelloData []*trello.Board, token string) (fullV
|
|||
log.Debugf("[Trello Migration] Converting card %s", card.ID)
|
||||
|
||||
// The usual stuff: Title, description, position, bucket id
|
||||
task := &models.Task{
|
||||
Title: card.Name,
|
||||
BucketID: bucketID,
|
||||
task := &models.TaskWithComments{
|
||||
Task: models.Task{
|
||||
Title: card.Name,
|
||||
BucketID: bucketID,
|
||||
},
|
||||
}
|
||||
|
||||
task.Description, err = convertMarkdownToHTML(card.Desc)
|
||||
|
@ -362,7 +393,32 @@ func convertTrelloDataToVikunja(trelloData []*trello.Board, token string) (fullV
|
|||
task.CoverImageAttachmentID = coverAttachment.ID
|
||||
}
|
||||
|
||||
project.Tasks = append(project.Tasks, &models.TaskWithComments{Task: *task})
|
||||
for _, action := range card.Actions {
|
||||
if action.DidCommentCard() {
|
||||
if task.Comments == nil {
|
||||
task.Comments = []*models.TaskComment{}
|
||||
}
|
||||
|
||||
comment := &models.TaskComment{
|
||||
Comment: action.Data.Text,
|
||||
Created: action.Date,
|
||||
Updated: action.Date,
|
||||
}
|
||||
|
||||
if currentMember == nil || action.IDMemberCreator != currentMember.ID {
|
||||
comment.Comment = "*" + action.MemberCreator.FullName + "*:\n\n" + comment.Comment
|
||||
}
|
||||
|
||||
comment.Comment, err = convertMarkdownToHTML(comment.Comment)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
task.Comments = append(task.Comments, comment)
|
||||
}
|
||||
}
|
||||
|
||||
project.Tasks = append(project.Tasks, task)
|
||||
}
|
||||
|
||||
project.Buckets = append(project.Buckets, bucket)
|
||||
|
@ -392,29 +448,62 @@ func (m *Migration) Migrate(u *user.User) (err error) {
|
|||
log.Debugf("[Trello Migration] Starting migration for user %d", u.ID)
|
||||
log.Debugf("[Trello Migration] Getting all trello data for user %d", u.ID)
|
||||
|
||||
trelloData, err := getTrelloData(m.Token)
|
||||
client := trello.NewClient(config.MigrationTrelloKey.GetString(), m.Token)
|
||||
client.Logger = log.GetLogger()
|
||||
|
||||
boards, err := getTrelloBoards(client)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("[Trello Migration] Got all trello data for user %d", u.ID)
|
||||
log.Debugf("[Trello Migration] Start converting trello data for user %d", u.ID)
|
||||
|
||||
fullVikunjaHierachie, err := convertTrelloDataToVikunja(trelloData, m.Token)
|
||||
if err != nil {
|
||||
return
|
||||
organizationMap := getTrelloOrganizationsWithBoards(boards)
|
||||
for organizationID, boards := range organizationMap {
|
||||
log.Debugf("[Trello Migration] Getting organization with id %s for user %d", organizationID, u.ID)
|
||||
orgName := organizationID
|
||||
if organizationID != "Personal" {
|
||||
organization, err := client.GetOrganization(organizationID, trello.Defaults())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
orgName = organization.DisplayName
|
||||
}
|
||||
|
||||
for _, board := range boards {
|
||||
log.Debugf("[Trello Migration] Getting card data for board %s for user %d for organization %s", board.ID, u.ID, organizationID)
|
||||
|
||||
err = fillCardData(client, board)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("[Trello Migration] Got card data for board %s for user %d for organization %s", board.ID, u.ID, organizationID)
|
||||
}
|
||||
|
||||
log.Debugf("[Trello Migration] Start converting trello data for user %d for organization %s", u.ID, organizationID)
|
||||
|
||||
currentMember, err := client.GetMyMember(trello.Defaults())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hierarchy, err := convertTrelloDataToVikunja(orgName, boards, client.Token, currentMember)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("[Trello Migration] Done migrating trello data for user %d for organization %s", u.ID, organizationID)
|
||||
log.Debugf("[Trello Migration] Start inserting trello data for user %d for organization %s", u.ID, organizationID)
|
||||
|
||||
err = migration.InsertFromStructure(hierarchy, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("[Trello Migration] Done inserting trello data for user %d for organization %s", u.ID, organizationID)
|
||||
}
|
||||
|
||||
log.Debugf("[Trello Migration] Done migrating trello data for user %d", u.ID)
|
||||
log.Debugf("[Trello Migration] Start inserting trello data for user %d", u.ID)
|
||||
log.Debugf("[Trello Migration] Done migrating all trello data for user %d", u.ID)
|
||||
|
||||
err = migration.InsertFromStructure(fullVikunjaHierachie, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("[Trello Migration] Done inserting trello data for user %d", u.ID)
|
||||
log.Debugf("[Trello Migration] Migration done for user %d", u.ID)
|
||||
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
|
|
@ -32,20 +32,23 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConvertTrelloToVikunja(t *testing.T) {
|
||||
func getTestBoard(t *testing.T) ([]*trello.Board, time.Time) {
|
||||
|
||||
config.InitConfig()
|
||||
|
||||
time1, err := time.Parse(time.RFC3339Nano, "2014-09-26T08:25:05Z")
|
||||
require.NoError(t, err)
|
||||
exampleFile, err := os.ReadFile(config.ServiceRootpath.GetString() + "/pkg/modules/migration/testimage.jpg")
|
||||
require.NoError(t, err)
|
||||
|
||||
trelloData := []*trello.Board{
|
||||
{
|
||||
Name: "TestBoard",
|
||||
Desc: "This is a description",
|
||||
Closed: false,
|
||||
Name: "TestBoard",
|
||||
Organization: trello.Organization{
|
||||
ID: "orgid",
|
||||
DisplayName: "TestOrg",
|
||||
},
|
||||
IDOrganization: "orgid",
|
||||
Desc: "This is a description",
|
||||
Closed: false,
|
||||
Lists: []*trello.List{
|
||||
{
|
||||
Name: "Test Project 1",
|
||||
|
@ -168,8 +171,13 @@ func TestConvertTrelloToVikunja(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
Name: "TestBoard 2",
|
||||
Closed: false,
|
||||
Organization: trello.Organization{
|
||||
ID: "orgid2",
|
||||
DisplayName: "TestOrg2",
|
||||
},
|
||||
IDOrganization: "orgid2",
|
||||
Name: "TestBoard 2",
|
||||
Closed: false,
|
||||
Lists: []*trello.List{
|
||||
{
|
||||
Name: "Test Project 4",
|
||||
|
@ -183,8 +191,13 @@ func TestConvertTrelloToVikunja(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
Name: "TestBoard Archived",
|
||||
Closed: true,
|
||||
Organization: trello.Organization{
|
||||
ID: "orgid",
|
||||
DisplayName: "TestOrg",
|
||||
},
|
||||
IDOrganization: "orgid",
|
||||
Name: "TestBoard Archived",
|
||||
Closed: true,
|
||||
Lists: []*trello.List{
|
||||
{
|
||||
Name: "Test Project 5",
|
||||
|
@ -197,67 +210,91 @@ func TestConvertTrelloToVikunja(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Personal Board",
|
||||
Lists: []*trello.List{
|
||||
{
|
||||
Name: "Test Project 6",
|
||||
Cards: []*trello.Card{
|
||||
{
|
||||
Name: "Test Card 5659",
|
||||
Pos: 123,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
trelloData[0].Prefs.BackgroundImage = "https://vikunja.io/testimage.jpg" // Using an image which we are hosting, so it'll still be up
|
||||
|
||||
expectedHierachie := []*models.ProjectWithTasksAndBuckets{
|
||||
{
|
||||
Project: models.Project{
|
||||
ID: 1,
|
||||
Title: "Imported from Trello",
|
||||
},
|
||||
},
|
||||
{
|
||||
Project: models.Project{
|
||||
ID: 2,
|
||||
ParentProjectID: 1,
|
||||
Title: "TestBoard",
|
||||
Description: "This is a description",
|
||||
BackgroundInformation: bytes.NewBuffer(exampleFile),
|
||||
},
|
||||
Buckets: []*models.Bucket{
|
||||
{
|
||||
return trelloData, time1
|
||||
}
|
||||
|
||||
func TestConvertTrelloToVikunja(t *testing.T) {
|
||||
trelloData, time1 := getTestBoard(t)
|
||||
|
||||
exampleFile, err := os.ReadFile(config.ServiceRootpath.GetString() + "/pkg/modules/migration/testimage.jpg")
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedHierarchyOrg := map[string][]*models.ProjectWithTasksAndBuckets{
|
||||
"orgid": {
|
||||
{
|
||||
Project: models.Project{
|
||||
ID: 1,
|
||||
Title: "Test Project 1",
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Title: "Test Project 2",
|
||||
Title: "orgid",
|
||||
},
|
||||
},
|
||||
Tasks: []*models.TaskWithComments{
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 1",
|
||||
Description: "<p>Card Description <strong>bold</strong></p>\n",
|
||||
BucketID: 1,
|
||||
DueDate: time1,
|
||||
Labels: []*models.Label{
|
||||
{
|
||||
Title: "Label 1",
|
||||
HexColor: trelloColorMap["green"],
|
||||
{
|
||||
Project: models.Project{
|
||||
ID: 2,
|
||||
ParentProjectID: 1,
|
||||
Title: "TestBoard",
|
||||
Description: "This is a description",
|
||||
BackgroundInformation: bytes.NewBuffer(exampleFile),
|
||||
},
|
||||
Buckets: []*models.Bucket{
|
||||
{
|
||||
ID: 1,
|
||||
Title: "Test Project 1",
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Title: "Test Project 2",
|
||||
},
|
||||
},
|
||||
Tasks: []*models.TaskWithComments{
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 1",
|
||||
Description: "<p>Card Description <strong>bold</strong></p>\n",
|
||||
BucketID: 1,
|
||||
DueDate: time1,
|
||||
Labels: []*models.Label{
|
||||
{
|
||||
Title: "Label 1",
|
||||
HexColor: trelloColorMap["green"],
|
||||
},
|
||||
{
|
||||
Title: "Label 2",
|
||||
HexColor: trelloColorMap["orange"],
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: "Label 2",
|
||||
HexColor: trelloColorMap["orange"],
|
||||
},
|
||||
},
|
||||
Attachments: []*models.TaskAttachment{
|
||||
{
|
||||
File: &files.File{
|
||||
Name: "Testimage.jpg",
|
||||
Mime: "image/jpg",
|
||||
Size: uint64(len(exampleFile)),
|
||||
FileContent: exampleFile,
|
||||
Attachments: []*models.TaskAttachment{
|
||||
{
|
||||
File: &files.File{
|
||||
Name: "Testimage.jpg",
|
||||
Mime: "image/jpg",
|
||||
Size: uint64(len(exampleFile)),
|
||||
FileContent: exampleFile,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 2",
|
||||
Description: `
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 2",
|
||||
Description: `
|
||||
|
||||
<h2> Checkproject 1</h2>
|
||||
|
||||
|
@ -270,117 +307,180 @@ func TestConvertTrelloToVikunja(t *testing.T) {
|
|||
<ul data-type="taskList">
|
||||
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label><div><p>Pending Task</p></div></li>
|
||||
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label><div><p>Another Pending Task</p></div></li></ul>`,
|
||||
BucketID: 1,
|
||||
BucketID: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 3",
|
||||
BucketID: 1,
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 3",
|
||||
BucketID: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 4",
|
||||
BucketID: 1,
|
||||
Labels: []*models.Label{
|
||||
{
|
||||
Title: "Label 2",
|
||||
HexColor: trelloColorMap["orange"],
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 4",
|
||||
BucketID: 1,
|
||||
Labels: []*models.Label{
|
||||
{
|
||||
Title: "Label 2",
|
||||
HexColor: trelloColorMap["orange"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 5",
|
||||
BucketID: 2,
|
||||
Labels: []*models.Label{
|
||||
{
|
||||
Title: "Label 3",
|
||||
HexColor: trelloColorMap["blue"],
|
||||
},
|
||||
{
|
||||
Title: "Label 4",
|
||||
HexColor: trelloColorMap["green_dark"],
|
||||
},
|
||||
{
|
||||
Title: "Label 5",
|
||||
HexColor: trelloColorMap["transparent"],
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 5",
|
||||
BucketID: 2,
|
||||
Labels: []*models.Label{
|
||||
{
|
||||
Title: "Label 3",
|
||||
HexColor: trelloColorMap["blue"],
|
||||
},
|
||||
{
|
||||
Title: "Label 4",
|
||||
HexColor: trelloColorMap["green_dark"],
|
||||
},
|
||||
{
|
||||
Title: "Label 5",
|
||||
HexColor: trelloColorMap["transparent"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 6",
|
||||
BucketID: 2,
|
||||
DueDate: time1,
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 6",
|
||||
BucketID: 2,
|
||||
DueDate: time1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 7",
|
||||
BucketID: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 8",
|
||||
BucketID: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 7",
|
||||
BucketID: 2,
|
||||
},
|
||||
{
|
||||
Project: models.Project{
|
||||
ID: 3,
|
||||
ParentProjectID: 1,
|
||||
Title: "TestBoard Archived",
|
||||
IsArchived: true,
|
||||
},
|
||||
Buckets: []*models.Bucket{
|
||||
{
|
||||
ID: 3,
|
||||
Title: "Test Project 5",
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 8",
|
||||
BucketID: 2,
|
||||
Tasks: []*models.TaskWithComments{
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 63423",
|
||||
BucketID: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Project: models.Project{
|
||||
ID: 3,
|
||||
ParentProjectID: 1,
|
||||
Title: "TestBoard 2",
|
||||
},
|
||||
Buckets: []*models.Bucket{
|
||||
{
|
||||
ID: 3,
|
||||
Title: "Test Project 4",
|
||||
"orgid2": {
|
||||
{
|
||||
Project: models.Project{
|
||||
ID: 1,
|
||||
Title: "orgid2",
|
||||
},
|
||||
},
|
||||
Tasks: []*models.TaskWithComments{
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 634",
|
||||
BucketID: 3,
|
||||
{
|
||||
Project: models.Project{
|
||||
ID: 2,
|
||||
ParentProjectID: 1,
|
||||
Title: "TestBoard 2",
|
||||
},
|
||||
Buckets: []*models.Bucket{
|
||||
{
|
||||
ID: 1,
|
||||
Title: "Test Project 4",
|
||||
},
|
||||
},
|
||||
Tasks: []*models.TaskWithComments{
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 634",
|
||||
BucketID: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Project: models.Project{
|
||||
ID: 4,
|
||||
ParentProjectID: 1,
|
||||
Title: "TestBoard Archived",
|
||||
IsArchived: true,
|
||||
},
|
||||
Buckets: []*models.Bucket{
|
||||
{
|
||||
ID: 4,
|
||||
Title: "Test Project 5",
|
||||
"Personal": {
|
||||
{
|
||||
Project: models.Project{
|
||||
ID: 1,
|
||||
Title: "Personal",
|
||||
},
|
||||
},
|
||||
Tasks: []*models.TaskWithComments{
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 63423",
|
||||
BucketID: 4,
|
||||
{
|
||||
Project: models.Project{
|
||||
ID: 2,
|
||||
ParentProjectID: 1,
|
||||
Title: "Personal Board",
|
||||
},
|
||||
Buckets: []*models.Bucket{
|
||||
{
|
||||
ID: 1,
|
||||
Title: "Test Project 6",
|
||||
},
|
||||
},
|
||||
Tasks: []*models.TaskWithComments{
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 5659",
|
||||
BucketID: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
hierachie, err := convertTrelloDataToVikunja(trelloData, "")
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, hierachie)
|
||||
if diff, equal := messagediff.PrettyDiff(hierachie, expectedHierachie); !equal {
|
||||
t.Errorf("converted trello data = %v, want %v, diff: %v", hierachie, expectedHierachie, diff)
|
||||
organizationMap := getTrelloOrganizationsWithBoards(trelloData)
|
||||
for organizationID, boards := range organizationMap {
|
||||
hierarchy, err := convertTrelloDataToVikunja(organizationID, boards, "", nil)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, hierarchy)
|
||||
if diff, equal := messagediff.PrettyDiff(hierarchy, expectedHierarchyOrg[organizationID]); !equal {
|
||||
t.Errorf("converted trello data = %v,\nwant %v,\ndiff: %v", hierarchy, expectedHierarchyOrg[organizationID], diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateOrganizationMap(t *testing.T) {
|
||||
trelloData, _ := getTestBoard(t)
|
||||
|
||||
organizationMap := getTrelloOrganizationsWithBoards(trelloData)
|
||||
expectedMap := map[string][]*trello.Board{
|
||||
"orgid": {
|
||||
trelloData[0],
|
||||
trelloData[2],
|
||||
},
|
||||
"orgid2": {
|
||||
trelloData[1],
|
||||
},
|
||||
"Personal": {
|
||||
trelloData[3],
|
||||
},
|
||||
}
|
||||
if diff, equal := messagediff.PrettyDiff(organizationMap, expectedMap); !equal {
|
||||
t.Errorf("converted trello data = %v,\nwant %v,\ndiff: %v", organizationMap, expectedMap, diff)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue