Compare commits

..

1 Commits

Author SHA1 Message Date
renovate 3b9d1d128d chore(deps): update dependency node to v20.12.1
continuous-integration/drone/pr Build is pending Details
2024-04-08 10:06:04 +00:00
10 changed files with 670 additions and 980 deletions

View File

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

View File

@ -10,7 +10,7 @@ menu:
# Vikunja Versions
Vikunja api is available in two different release flavors.
The Vikunja api and frontend are available in two different release flavors.
{{< table_of_contents >}}
@ -30,9 +30,6 @@ 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.2
20.12.1

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.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",
"@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",
"@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.1.0",
"dompurify": "3.0.11",
"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.11.0",
"vue-i18n": "9.10.2",
"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.17",
"@histoire/plugin-vue": "0.17.17",
"@histoire/plugin-screenshot": "0.17.15",
"@histoire/plugin-vue": "0.17.15",
"@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.7",
"@types/node": "20.12.5",
"@types/postcss-preset-env": "7.7.0",
"@types/sortablejs": "1.15.8",
"@typescript-eslint/eslint-plugin": "7.6.0",
"@typescript-eslint/parser": "7.6.0",
"@typescript-eslint/eslint-plugin": "7.5.0",
"@typescript-eslint/parser": "7.5.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.30001608",
"caniuse-lite": "1.0.30001607",
"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.1",
"eslint-plugin-vue": "9.24.0",
"happy-dom": "14.7.1",
"histoire": "0.17.17",
"histoire": "0.17.15",
"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.5",
"typescript": "5.4.4",
"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.12",
"vue-tsc": "2.0.11",
"wait-on": "7.2.0",
"workbox-cli": "7.0.0"
},

File diff suppressed because it is too large Load Diff

View File

@ -155,6 +155,7 @@ 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,32 +18,19 @@ 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 {
}
@ -72,46 +59,13 @@ func (s *MigrationListener) Handle(msg *message.Message) (err error) {
ms := event.Migrator.(migration.Migrator)
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)
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
@ -119,7 +73,6 @@ func migrateInListener(ms migration.Migrator, event *MigrationRequestedEvent) (m
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
}
@ -127,7 +80,6 @@ func migrateInListener(ms migration.Migrator, event *MigrationRequestedEvent) (m
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,57 +49,3 @@ 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,109 +107,80 @@ func (m *Migration) AuthURL() string {
"&return_url=" + config.MigrationTrelloRedirectURL.GetString()
}
func getTrelloBoards(client *trello.Client) (trelloData []*trello.Board, err error) {
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()
log.Debugf("[Trello Migration] Getting boards...")
trelloData, err = client.GetMyBoards(trello.Defaults())
if err != nil {
return nil, err
return
}
log.Debugf("[Trello Migration] Got %d trello boards", len(trelloData))
return
}
for _, board := range trelloData {
log.Debugf("[Trello Migration] Getting projects for board %s", board.ID)
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"
board.Lists, err = board.GetLists(trello.Defaults())
if err != nil {
return
}
_, has := boardsByOrg[board.IDOrganization]
if !has {
boardsByOrg[board.IDOrganization] = []*trello.Board{}
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
}
boardsByOrg[board.IDOrganization] = append(boardsByOrg[board.IDOrganization], board)
}
log.Debugf("[Trello Migration] Getting cards for board %s", board.ID)
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
cards, err := board.GetCards(allArg)
if err != nil {
return nil, err
}
if card.Badges.Attachments > 0 {
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
}
card.Attachments, err = card.GetAttachments(allArg)
if err != nil {
return
return nil, err
}
}
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 nil, err
}
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 nil, err
}
card.Checklists = append(card.Checklists, checklist)
log.Debugf("Retrieved checklist %s for card %s", checkListID, card.ID)
}
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)
}
list.Cards = append(list.Cards, card)
log.Debugf("[Trello Migration] Looked for attachements on all cards of board %s", board.ID)
}
log.Debugf("[Trello Migration] Looked for attachements on all cards of board %s", board.ID)
return
}
@ -225,7 +196,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(organizationName string, trelloData []*trello.Board, token string, currentMember *trello.Member) (fullVikunjaHierachie []*models.ProjectWithTasksAndBuckets, err error) {
func convertTrelloDataToVikunja(trelloData []*trello.Board, token string) (fullVikunjaHierachie []*models.ProjectWithTasksAndBuckets, err error) {
log.Debugf("[Trello Migration] ")
@ -234,7 +205,7 @@ func convertTrelloDataToVikunja(organizationName string, trelloData []*trello.Bo
{
Project: models.Project{
ID: pseudoParentID,
Title: organizationName,
Title: "Imported from Trello",
},
},
}
@ -281,11 +252,9 @@ func convertTrelloDataToVikunja(organizationName string, trelloData []*trello.Bo
log.Debugf("[Trello Migration] Converting card %s", card.ID)
// The usual stuff: Title, description, position, bucket id
task := &models.TaskWithComments{
Task: models.Task{
Title: card.Name,
BucketID: bucketID,
},
task := &models.Task{
Title: card.Name,
BucketID: bucketID,
}
task.Description, err = convertMarkdownToHTML(card.Desc)
@ -393,32 +362,7 @@ func convertTrelloDataToVikunja(organizationName string, trelloData []*trello.Bo
task.CoverImageAttachmentID = coverAttachment.ID
}
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.Tasks = append(project.Tasks, &models.TaskWithComments{Task: *task})
}
project.Buckets = append(project.Buckets, bucket)
@ -448,62 +392,29 @@ 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)
client := trello.NewClient(config.MigrationTrelloKey.GetString(), m.Token)
client.Logger = log.GetLogger()
boards, err := getTrelloBoards(client)
trelloData, err := getTrelloData(m.Token)
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)
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)
fullVikunjaHierachie, err := convertTrelloDataToVikunja(trelloData, m.Token)
if err != nil {
return
}
log.Debugf("[Trello Migration] Done migrating all trello data for user %d", u.ID)
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)
return
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
}

View File

@ -32,23 +32,20 @@ import (
"github.com/stretchr/testify/require"
)
func getTestBoard(t *testing.T) ([]*trello.Board, time.Time) {
func TestConvertTrelloToVikunja(t *testing.T) {
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",
Organization: trello.Organization{
ID: "orgid",
DisplayName: "TestOrg",
},
IDOrganization: "orgid",
Desc: "This is a description",
Closed: false,
Name: "TestBoard",
Desc: "This is a description",
Closed: false,
Lists: []*trello.List{
{
Name: "Test Project 1",
@ -171,13 +168,8 @@ func getTestBoard(t *testing.T) ([]*trello.Board, time.Time) {
},
},
{
Organization: trello.Organization{
ID: "orgid2",
DisplayName: "TestOrg2",
},
IDOrganization: "orgid2",
Name: "TestBoard 2",
Closed: false,
Name: "TestBoard 2",
Closed: false,
Lists: []*trello.List{
{
Name: "Test Project 4",
@ -191,13 +183,8 @@ func getTestBoard(t *testing.T) ([]*trello.Board, time.Time) {
},
},
{
Organization: trello.Organization{
ID: "orgid",
DisplayName: "TestOrg",
},
IDOrganization: "orgid",
Name: "TestBoard Archived",
Closed: true,
Name: "TestBoard Archived",
Closed: true,
Lists: []*trello.List{
{
Name: "Test Project 5",
@ -210,91 +197,67 @@ func getTestBoard(t *testing.T) ([]*trello.Board, time.Time) {
},
},
},
{
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
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{
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{
{
ID: 1,
Title: "orgid",
Title: "Test Project 1",
},
{
ID: 2,
Title: "Test Project 2",
},
},
{
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"],
},
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"],
},
Attachments: []*models.TaskAttachment{
{
File: &files.File{
Name: "Testimage.jpg",
Mime: "image/jpg",
Size: uint64(len(exampleFile)),
FileContent: exampleFile,
},
{
Title: "Label 2",
HexColor: trelloColorMap["orange"],
},
},
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>
@ -307,180 +270,117 @@ 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 7",
BucketID: 2,
},
},
{
Task: models.Task{
Title: "Test Card 8",
BucketID: 2,
},
},
{
Task: models.Task{
Title: "Test Card 6",
BucketID: 2,
DueDate: time1,
},
},
},
{
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 7",
BucketID: 2,
},
},
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Test Card 63423",
BucketID: 3,
},
{
Task: models.Task{
Title: "Test Card 8",
BucketID: 2,
},
},
},
},
"orgid2": {
{
Project: models.Project{
ID: 1,
Title: "orgid2",
{
Project: models.Project{
ID: 3,
ParentProjectID: 1,
Title: "TestBoard 2",
},
Buckets: []*models.Bucket{
{
ID: 3,
Title: "Test Project 4",
},
},
{
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,
},
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Test Card 634",
BucketID: 3,
},
},
},
},
"Personal": {
{
Project: models.Project{
ID: 1,
Title: "Personal",
{
Project: models.Project{
ID: 4,
ParentProjectID: 1,
Title: "TestBoard Archived",
IsArchived: true,
},
Buckets: []*models.Bucket{
{
ID: 4,
Title: "Test Project 5",
},
},
{
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,
},
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Test Card 63423",
BucketID: 4,
},
},
},
},
}
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)
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)
}
}