Compare commits

..

12 Commits

Author SHA1 Message Date
renovate d51f340f7b chore(deps): update dependency node
continuous-integration/drone/pr Build is pending Details
2024-04-10 17:07:39 +00:00
renovate e19ac57130 chore(deps): update dev-dependencies
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-04-10 15:06:07 +00:00
kolaente 0557d4b5bb
docs: clarify transitioning from unstable to release
continuous-integration/drone/push Build is passing Details
2024-04-09 22:43:27 +02:00
kolaente bc19a2fb78
fix(migration): import card comments from Trello when migrating
continuous-integration/drone/push Build is passing Details
Related: https://community.vikunja.io/t/trello-import-comments-and-assignments/2174/3
2024-04-09 13:56:17 +02:00
kolaente 994aaeb920
fix(migration): trello: only fetch attachments when the card actually has attachments 2024-04-09 13:25:03 +02:00
kolaente ee3d20e1d2
fix(navigation): do not hide shadows of dropdown menu
continuous-integration/drone/push Build is passing Details
2024-04-09 13:07:01 +02:00
Elscrux 8458e77341 feat(migration): Trello organization based migration (#2211)
continuous-integration/drone/push Build is failing Details
Migrate Trello organization after organization to limit total memory allocation.
Related discussion: https://community.vikunja.io/t/trello-import-issues/2110

Co-authored-by: Elscrux <nickposer2102@gmail.com>
Co-authored-by: konrad <k@knt.li>
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: #2211
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Elscrux <elscrux@gmail.com>
Co-committed-by: Elscrux <elscrux@gmail.com>
2024-04-09 10:54:38 +00:00
kolaente af3b0bbea1
fix: lint
continuous-integration/drone/push Build is passing Details
2024-04-08 13:23:15 +02:00
renovate d2317b9531 fix(deps): update dependency vue-i18n to v9.11.0
continuous-integration/drone/push Build is failing Details
2024-04-08 10:35:09 +00:00
renovate 552f8580a4 fix(deps): update dependency dompurify to v3.1.0
continuous-integration/drone/push Build is failing Details
2024-04-08 10:27:31 +00:00
renovate c842cb27b2 fix(deps): update tiptap to v2.2.6
continuous-integration/drone/push Build is failing Details
2024-04-08 10:19:11 +00:00
kolaente e10cd368bf
feat(migration): notify the user when a migration failed
continuous-integration/drone/push Build is failing Details
This change introduces notifications via mail when a migration fails. It will contain the error message and a hint to post it in the forum when Sentry is disabled, otherwise the error message will be sent directly to sentry and the notification will inform accordingly.
I've tried to balance "this thing failed, go figure it out" with "here is what we know and how you can get help", we'll see how well that approach works.
2024-04-08 12:15:24 +02:00
10 changed files with 984 additions and 674 deletions

View File

@ -5,7 +5,7 @@
"eslint.packageManager": "pnpm",
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll": true
"source.fixAll": "explicit"
},
"eslint.format.enable": true,
"[javascript]": {

View File

@ -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

View File

@ -1 +1 @@
20.12.1
20.12.2

View File

@ -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

View File

@ -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) {

View File

@ -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
}

View File

@ -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"
}

View File

@ -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
}

View File

@ -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)
}
}