From ffa82556e0a8837b127b8b5385b1765bb4b72e4c Mon Sep 17 00:00:00 2001 From: waza-ari Date: Sun, 10 Mar 2024 14:04:32 +0000 Subject: [PATCH 01/66] feat(teams): add public flags to teams to allow easier sharing with other teams (#2179) Resolves #2173 Co-authored-by: Daniel Herrmann Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2179 Reviewed-by: konrad Co-authored-by: waza-ari Co-committed-by: waza-ari --- config.yml.sample | 3 + docs/content/doc/setup/config.md | 11 +++ docs/content/doc/setup/openid.md | 17 ++++- frontend/src/components/sharing/userTeam.vue | 13 +++- frontend/src/i18n/lang/en.json | 4 +- frontend/src/modelTypes/ITeam.ts | 1 + frontend/src/models/team.ts | 1 + frontend/src/stores/config.ts | 2 + frontend/src/views/teams/EditTeam.vue | 24 +++++++ frontend/src/views/teams/NewTeam.vue | 25 +++++++ pkg/config/config.go | 2 + pkg/db/fixtures/team_members.yml | 50 +++++-------- pkg/db/fixtures/teams.yml | 10 ++- pkg/migration/20240309111148.go | 43 +++++++++++ pkg/models/teams.go | 25 ++++++- pkg/models/teams_test.go | 75 ++++++++++++++++++++ pkg/modules/auth/openid/openid.go | 36 ++++++++-- pkg/modules/auth/openid/openid_test.go | 37 +++++++++- pkg/routes/api/v1/info.go | 2 + pkg/swagger/docs.go | 19 +++++ pkg/swagger/swagger.json | 19 +++++ pkg/swagger/swagger.yaml | 18 +++++ 22 files changed, 392 insertions(+), 45 deletions(-) create mode 100644 pkg/migration/20240309111148.go diff --git a/config.yml.sample b/config.yml.sample index acbb5a300..10f30b0d4 100644 --- a/config.yml.sample +++ b/config.yml.sample @@ -62,6 +62,9 @@ service: allowiconchanges: true # Allow using a custom logo via external URL. customlogourl: '' + # Enables the public team feature. If enabled, it is possible to configure teams to be public, which makes them + # discoverable when sharing a project, therefore not only showing teams the user is member of. + enablepublicteams: false sentry: # If set to true, enables anonymous error tracking of api errors via Sentry. This allows us to gather more diff --git a/docs/content/doc/setup/config.md b/docs/content/doc/setup/config.md index a68bf22b5..c5210edec 100644 --- a/docs/content/doc/setup/config.md +++ b/docs/content/doc/setup/config.md @@ -346,6 +346,17 @@ Full path: `service.customlogourl` Environment path: `VIKUNJA_SERVICE_CUSTOMLOGOURL` +### enablepublicteams + +discoverable when sharing a project, therefore not only showing teams the user is member of. + +Default: `false` + +Full path: `service.enablepublicteams` + +Environment path: `VIKUNJA_SERVICE_ENABLEPUBLICTEAMS` + + --- ## sentry diff --git a/docs/content/doc/setup/openid.md b/docs/content/doc/setup/openid.md index 320c2a725..b0e66e69c 100644 --- a/docs/content/doc/setup/openid.md +++ b/docs/content/doc/setup/openid.md @@ -99,7 +99,7 @@ It depends on the provider being used as well as the preferences of the administ Typically you'd want to request an additional scope (e.g. `vikunja_scope`) which then triggers the identity provider to add the claim. If the `vikunja_groups` is part of the **ID token**, Vikunja will start the procedure and import teams and team memberships. -The claim structure expexted by Vikunja is as follows: +The minimal claim structure expected by Vikunja is as follows: ```json { @@ -116,6 +116,21 @@ The claim structure expexted by Vikunja is as follows: } ``` +It also also possible to pass the description and isPublic flag as optional parameter. If not present, the description will be empty and project visibility defaults to false. + +```json +{ + "vikunja_groups": [ + { + "name": "team 3", + "oidcID": 33349, + "description": "My Team Description", + "isPublic": true + }, + ] +} +``` + For each team, you need to define a team `name` and an `oidcID`, where the `oidcID` can be any string with a length of less than 250 characters. The `oidcID` is used to uniquely identify the team, so please make sure to keep this unique. diff --git a/frontend/src/components/sharing/userTeam.vue b/frontend/src/components/sharing/userTeam.vue index 3858c6ab5..535b8cdbb 100644 --- a/frontend/src/components/sharing/userTeam.vue +++ b/frontend/src/components/sharing/userTeam.vue @@ -172,6 +172,7 @@ import Multiselect from '@/components/input/multiselect.vue' import Nothing from '@/components/misc/nothing.vue' import {success} from '@/message' import {useAuthStore} from '@/stores/auth' +import {useConfigStore} from '@/stores/config' // FIXME: I think this whole thing can now only manage user/team sharing for projects? Maybe remove a little generalization? @@ -210,8 +211,8 @@ const selectedRight = ref({}) const sharables = ref([]) const showDeleteModal = ref(false) - const authStore = useAuthStore() +const configStore = useConfigStore() const userInfo = computed(() => authStore.info) function createShareTypeNameComputed(count: number) { @@ -360,7 +361,15 @@ async function find(query: string) { found.value = [] return } - const results = await searchService.getAll({}, {s: query}) + + // Include public teams here if we are sharing with teams and its enabled in the config + let results = [] + if (props.shareType === 'team' && configStore.publicTeamsEnabled) { + results = await searchService.getAll({}, {s: query, includePublic: true}) + } else { + results = await searchService.getAll({}, {s: query}) + } + found.value = results .filter(m => { if(props.shareType === 'user' && m.id === currentUserId.value) { diff --git a/frontend/src/i18n/lang/en.json b/frontend/src/i18n/lang/en.json index 652b43450..b7747ecb7 100644 --- a/frontend/src/i18n/lang/en.json +++ b/frontend/src/i18n/lang/en.json @@ -986,7 +986,9 @@ "description": "Description", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "Admin", - "member": "Member" + "member": "Member", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { diff --git a/frontend/src/modelTypes/ITeam.ts b/frontend/src/modelTypes/ITeam.ts index 3cdaae987..e9e7142cb 100644 --- a/frontend/src/modelTypes/ITeam.ts +++ b/frontend/src/modelTypes/ITeam.ts @@ -10,6 +10,7 @@ export interface ITeam extends IAbstract { members: ITeamMember[] right: Right oidcId: string + isPublic: boolean createdBy: IUser created: Date diff --git a/frontend/src/models/team.ts b/frontend/src/models/team.ts index 1e75738bb..cc17849fa 100644 --- a/frontend/src/models/team.ts +++ b/frontend/src/models/team.ts @@ -14,6 +14,7 @@ export default class TeamModel extends AbstractModel implements ITeam { members: ITeamMember[] = [] right: Right = RIGHTS.READ oidcId = '' + isPublic: boolean = false createdBy: IUser = {} // FIXME: seems wrong created: Date = null diff --git a/frontend/src/stores/config.ts b/frontend/src/stores/config.ts index c5dbe6973..eb09372da 100644 --- a/frontend/src/stores/config.ts +++ b/frontend/src/stores/config.ts @@ -37,6 +37,7 @@ export interface ConfigState { providers: IProvider[], }, }, + publicTeamsEnabled: boolean, } export const useConfigStore = defineStore('config', () => { @@ -70,6 +71,7 @@ export const useConfigStore = defineStore('config', () => { providers: [], }, }, + publicTeamsEnabled: false, }) const migratorsEnabled = computed(() => state.availableMigrators?.length > 0) diff --git a/frontend/src/views/teams/EditTeam.vue b/frontend/src/views/teams/EditTeam.vue index 993f2e6af..f435d4df8 100644 --- a/frontend/src/views/teams/EditTeam.vue +++ b/frontend/src/views/teams/EditTeam.vue @@ -33,6 +33,27 @@ > {{ $t('team.attributes.nameRequired') }}

+
+ +
+ + {{ $t('team.attributes.isPublicDescription') }} + +
+
+
+ +
+ + {{ $t('team.attributes.isPublicDescription') }} + +
+

t('team.create.title')) useTitle(title) @@ -60,6 +83,8 @@ const teamService = shallowReactive(new TeamService()) const team = reactive(new TeamModel()) const showError = ref(false) +const configStore = useConfigStore() + async function newTeam() { if (team.name === '') { showError.value = true diff --git a/pkg/config/config.go b/pkg/config/config.go index 0e82a6637..d8f904e52 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -64,6 +64,7 @@ const ( ServiceMaxAvatarSize Key = `service.maxavatarsize` ServiceAllowIconChanges Key = `service.allowiconchanges` ServiceCustomLogoURL Key = `service.customlogourl` + ServiceEnablePublicTeams Key = `service.enablepublicteams` SentryEnabled Key = `sentry.enabled` SentryDsn Key = `sentry.dsn` @@ -312,6 +313,7 @@ func InitDefaultConfig() { ServiceMaxAvatarSize.setDefault(1024) ServiceDemoMode.setDefault(false) ServiceAllowIconChanges.setDefault(true) + ServiceEnablePublicTeams.setDefault(false) // Sentry SentryDsn.setDefault("https://440eedc957d545a795c17bbaf477497c@o1047380.ingest.sentry.io/4504254983634944") diff --git a/pkg/db/fixtures/team_members.yml b/pkg/db/fixtures/team_members.yml index 889322b7b..2b260b329 100644 --- a/pkg/db/fixtures/team_members.yml +++ b/pkg/db/fixtures/team_members.yml @@ -1,61 +1,49 @@ -- - team_id: 1 +- team_id: 1 user_id: 1 admin: true created: 2018-12-01 15:13:12 -- - team_id: 1 +- team_id: 1 user_id: 2 created: 2018-12-01 15:13:12 -- - team_id: 2 +- team_id: 2 user_id: 1 created: 2018-12-01 15:13:12 -- - team_id: 3 +- team_id: 3 user_id: 1 created: 2018-12-01 15:13:12 -- - team_id: 4 +- team_id: 4 user_id: 1 created: 2018-12-01 15:13:12 -- - team_id: 5 +- team_id: 5 user_id: 1 created: 2018-12-01 15:13:12 -- - team_id: 6 +- team_id: 6 user_id: 1 created: 2018-12-01 15:13:12 -- - team_id: 7 +- team_id: 7 user_id: 1 created: 2018-12-01 15:13:12 -- - team_id: 8 +- team_id: 8 user_id: 1 created: 2018-12-01 15:13:12 -- - team_id: 9 +- team_id: 9 user_id: 2 created: 2018-12-01 15:13:12 -- - team_id: 10 +- team_id: 10 user_id: 3 created: 2018-12-01 15:13:12 -- - team_id: 11 +- team_id: 11 user_id: 8 created: 2018-12-01 15:13:12 -- - team_id: 12 +- team_id: 12 user_id: 9 created: 2018-12-01 15:13:12 -- - team_id: 13 +- team_id: 13 user_id: 10 created: 2018-12-01 15:13:12 -- - team_id: 14 +- team_id: 14 user_id: 10 - created: 2018-12-01 15:13:12 \ No newline at end of file + created: 2018-12-01 15:13:12 +- team_id: 15 + user_id: 10 + created: 2018-12-01 15:13:12 diff --git a/pkg/db/fixtures/teams.yml b/pkg/db/fixtures/teams.yml index aaba624a3..5c4f49ec7 100644 --- a/pkg/db/fixtures/teams.yml +++ b/pkg/db/fixtures/teams.yml @@ -29,8 +29,16 @@ - id: 13 name: testteam13 created_by_id: 7 + is_public: true - id: 14 name: testteam14 created_by_id: 7 oidc_id: 14 - issuer: "https://some.issuer" \ No newline at end of file + issuer: "https://some.issuer" +- id: 15 + name: testteam15 + created_by_id: 7 + oidc_id: 15 + issuer: "https://some.issuer" + is_public: true + description: "This is a public team" diff --git a/pkg/migration/20240309111148.go b/pkg/migration/20240309111148.go new file mode 100644 index 000000000..7b0aa4c83 --- /dev/null +++ b/pkg/migration/20240309111148.go @@ -0,0 +1,43 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package migration + +import ( + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" +) + +type teams20240309111148 struct { + IsPublic bool `xorm:"not null default false" json:"is_public"` +} + +func (teams20240309111148) TableName() string { + return "teams" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20240309111148", + Description: "Add IsPublic field to teams table to control discoverability of teams.", + Migrate: func(tx *xorm.Engine) error { + return tx.Sync2(teams20240309111148{}) + }, + Rollback: func(tx *xorm.Engine) error { + return nil + }, + }) +} diff --git a/pkg/models/teams.go b/pkg/models/teams.go index 648cbe919..8cae52324 100644 --- a/pkg/models/teams.go +++ b/pkg/models/teams.go @@ -19,6 +19,7 @@ package models import ( "time" + "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/events" @@ -53,6 +54,12 @@ type Team struct { // A timestamp when this relation was last updated. You cannot change this value. Updated time.Time `xorm:"updated" json:"updated"` + // Defines wether the team should be publicly discoverable when sharing a project + IsPublic bool `xorm:"not null default false" json:"is_public"` + + // Query parameter controlling whether to include public projects or not + IncludePublic bool `xorm:"-" query:"include_public" json:"include_public"` + web.CRUDable `xorm:"-" json:"-"` web.Rights `xorm:"-" json:"-"` } @@ -100,6 +107,7 @@ type OIDCTeam struct { Name string OidcID string Description string + IsPublic bool } // GetTeamByID gets a team by its ID @@ -287,11 +295,24 @@ func (t *Team) ReadAll(s *xorm.Session, a web.Auth, search string, page int, per limit, start := getLimitFromPageIndex(page, perPage) all := []*Team{} + query := s.Select("teams.*"). Table("teams"). Join("INNER", "team_members", "team_members.team_id = teams.id"). - Where("team_members.user_id = ?", a.GetID()). Where(db.ILIKE("teams.name", search)) + + // If public teams are enabled, we want to include them in the result + if config.ServiceEnablePublicTeams.GetBool() && t.IncludePublic { + query = query.Where( + builder.Or( + builder.Eq{"teams.is_public": true}, + builder.Eq{"team_members.user_id": a.GetID()}, + ), + ) + } else { + query = query.Where("team_members.user_id = ?", a.GetID()) + } + if limit > 0 { query = query.Limit(limit, start) } @@ -398,7 +419,7 @@ func (t *Team) Update(s *xorm.Session, _ web.Auth) (err error) { return } - _, err = s.ID(t.ID).Update(t) + _, err = s.ID(t.ID).UseBool("is_public").Update(t) if err != nil { return } diff --git a/pkg/models/teams_test.go b/pkg/models/teams_test.go index 627cbc249..4f0ccd2d2 100644 --- a/pkg/models/teams_test.go +++ b/pkg/models/teams_test.go @@ -20,6 +20,7 @@ import ( "reflect" "testing" + "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/user" @@ -49,6 +50,7 @@ func TestTeam_Create(t *testing.T) { "id": team.ID, "name": "Testteam293", "description": "Lorem Ispum", + "is_public": false, }, false) }) t.Run("empty name", func(t *testing.T) { @@ -61,6 +63,27 @@ func TestTeam_Create(t *testing.T) { require.Error(t, err) assert.True(t, IsErrTeamNameCannotBeEmpty(err)) }) + t.Run("public", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + team := &Team{ + Name: "Testteam293_Public", + Description: "Lorem Ispum", + IsPublic: true, + } + err := team.Create(s, doer) + require.NoError(t, err) + err = s.Commit() + require.NoError(t, err) + db.AssertExists(t, "teams", map[string]interface{}{ + "id": team.ID, + "name": "Testteam293_Public", + "description": "Lorem Ispum", + "is_public": true, + }, false) + }) } func TestTeam_ReadOne(t *testing.T) { @@ -126,6 +149,58 @@ func TestTeam_ReadAll(t *testing.T) { assert.Len(t, ts, 1) assert.Equal(t, int64(2), ts[0].ID) }) + t.Run("public discovery disabled", func(t *testing.T) { + + s := db.NewSession() + defer s.Close() + + team := &Team{} + + // Default setting is having ServiceEnablePublicTeams disabled + // In this default case, fetching teams with or without public flag should return the same result + + // Fetch without public flag + teams, _, _, err := team.ReadAll(s, doer, "", 1, 50) + require.NoError(t, err) + assert.Equal(t, reflect.Slice, reflect.TypeOf(teams).Kind()) + ts := teams.([]*Team) + assert.Len(t, ts, 5) + + // Fetch with public flag + team.IncludePublic = true + teams, _, _, err = team.ReadAll(s, doer, "", 1, 50) + require.NoError(t, err) + assert.Equal(t, reflect.Slice, reflect.TypeOf(teams).Kind()) + ts = teams.([]*Team) + assert.Len(t, ts, 5) + }) + + t.Run("public discovery enabled", func(t *testing.T) { + + s := db.NewSession() + defer s.Close() + + team := &Team{} + + // Enable ServiceEnablePublicTeams feature + config.ServiceEnablePublicTeams.Set(true) + + // Fetch without public flag should be the same as before + team.IncludePublic = false + teams, _, _, err := team.ReadAll(s, doer, "", 1, 50) + require.NoError(t, err) + assert.Equal(t, reflect.Slice, reflect.TypeOf(teams).Kind()) + ts := teams.([]*Team) + assert.Len(t, ts, 5) + + // Fetch with public flag should return more teams + team.IncludePublic = true + teams, _, _, err = team.ReadAll(s, doer, "", 1, 50) + require.NoError(t, err) + assert.Equal(t, reflect.Slice, reflect.TypeOf(teams).Kind()) + ts = teams.([]*Team) + assert.Len(t, ts, 7) + }) } func TestTeam_Update(t *testing.T) { diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index 1da50c06b..ad9655315 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -298,14 +298,27 @@ func getTeamDataFromToken(groups []map[string]interface{}, provider *Provider) ( var name string var description string var oidcID string + var IsPublic bool + + // Read name _, exists := team["name"] if exists { name = team["name"].(string) } + + // Read description _, exists = team["description"] if exists { description = team["description"].(string) } + + // Read isPublic flag + _, exists = team["isPublic"] + if exists { + IsPublic = team["isPublic"].(bool) + } + + // Read oidcID _, exists = team["oidcID"] if exists { switch t := team["oidcID"].(type) { @@ -324,7 +337,7 @@ func getTeamDataFromToken(groups []map[string]interface{}, provider *Provider) ( errs = append(errs, &user.ErrOpenIDCustomScopeMalformed{}) continue } - teamData = append(teamData, &models.OIDCTeam{Name: name, OidcID: oidcID, Description: description}) + teamData = append(teamData, &models.OIDCTeam{Name: name, OidcID: oidcID, Description: description, IsPublic: IsPublic}) } return teamData, errs } @@ -339,6 +352,7 @@ func CreateOIDCTeam(s *xorm.Session, teamData *models.OIDCTeam, u *user.User, is Description: teamData.Description, OidcID: teamData.OidcID, Issuer: issuer, + IsPublic: teamData.IsPublic, } err = team.CreateNewTeam(s, u, false) return team, err @@ -363,12 +377,24 @@ func GetOrCreateTeamsByOIDC(s *xorm.Session, teamData []*models.OIDCTeam, u *use continue } + // Compare the name and update if it changed if team.Name != getOIDCTeamName(oidcTeam.Name) { team.Name = getOIDCTeamName(oidcTeam.Name) - err = team.Update(s, u) - if err != nil { - return nil, err - } + } + + // Compare the description and update if it changed + if team.Description != oidcTeam.Description { + team.Description = oidcTeam.Description + } + + // Compare the isPublic flag and update if it changed + if team.IsPublic != oidcTeam.IsPublic { + team.IsPublic = oidcTeam.IsPublic + } + + err = team.Update(s, u) + if err != nil { + return nil, err } log.Debugf("Team with oidc_id %v and name %v already exists.", team.OidcID, team.Name) diff --git a/pkg/modules/auth/openid/openid_test.go b/pkg/modules/auth/openid/openid_test.go index eb234600d..b1ebcdaa7 100644 --- a/pkg/modules/auth/openid/openid_test.go +++ b/pkg/modules/auth/openid/openid_test.go @@ -128,8 +128,41 @@ func TestGetOrCreateUser(t *testing.T) { "email": cl.Email, }, false) db.AssertExists(t, "teams", map[string]interface{}{ - "id": oidcTeams, - "name": team + " (OIDC)", + "id": oidcTeams, + "name": team + " (OIDC)", + "is_public": false, + }, false) + }) + + t.Run("Update IsPublic flag for existing team", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + team := "testteam15" + oidcID := "15" + cl := &claims{ + Email: "other-email-address@some.service.com", + VikunjaGroups: []map[string]interface{}{ + {"name": team, "oidcID": oidcID, "isPublic": true}, + }, + } + + u, err := getOrCreateUser(s, cl, "https://some.service.com", "12345") + require.NoError(t, err) + teamData, errs := getTeamDataFromToken(cl.VikunjaGroups, nil) + for _, err := range errs { + require.NoError(t, err) + } + oidcTeams, err := AssignOrCreateUserToTeams(s, u, teamData, "https://some.issuer") + require.NoError(t, err) + err = s.Commit() + require.NoError(t, err) + + db.AssertExists(t, "teams", map[string]interface{}{ + "id": oidcTeams, + "name": team + " (OIDC)", + "is_public": true, }, false) }) diff --git a/pkg/routes/api/v1/info.go b/pkg/routes/api/v1/info.go index 59958b0dc..8bd16e76b 100644 --- a/pkg/routes/api/v1/info.go +++ b/pkg/routes/api/v1/info.go @@ -51,6 +51,7 @@ type vikunjaInfos struct { TaskCommentsEnabled bool `json:"task_comments_enabled"` DemoModeEnabled bool `json:"demo_mode_enabled"` WebhooksEnabled bool `json:"webhooks_enabled"` + PublicTeamsEnabled bool `json:"public_teams_enabled"` } type authInfo struct { @@ -95,6 +96,7 @@ func Info(c echo.Context) error { TaskCommentsEnabled: config.ServiceEnableTaskComments.GetBool(), DemoModeEnabled: config.ServiceDemoMode.GetBool(), WebhooksEnabled: config.WebhooksEnabled.GetBool(), + PublicTeamsEnabled: config.ServiceEnablePublicTeams.GetBool(), AvailableMigrators: []string{ (&vikunja_file.FileMigrator{}).Name(), (&ticktick.Migrator{}).Name(), diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 76a185175..b67243664 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -8229,6 +8229,14 @@ const docTemplate = `{ "description": "The unique, numeric id of this team.", "type": "integer" }, + "include_public": { + "description": "Query parameter controlling whether to include public projects or not", + "type": "boolean" + }, + "is_public": { + "description": "Defines wether the team should be publicly discoverable when sharing a project", + "type": "boolean" + }, "members": { "description": "An array of all members in this team.", "type": "array", @@ -8364,6 +8372,14 @@ const docTemplate = `{ "description": "The unique, numeric id of this team.", "type": "integer" }, + "include_public": { + "description": "Query parameter controlling whether to include public projects or not", + "type": "boolean" + }, + "is_public": { + "description": "Defines wether the team should be publicly discoverable when sharing a project", + "type": "boolean" + }, "members": { "description": "An array of all members in this team.", "type": "array", @@ -8886,6 +8902,9 @@ const docTemplate = `{ "motd": { "type": "string" }, + "public_teams_enabled": { + "type": "boolean" + }, "registration_enabled": { "type": "boolean" }, diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 7edf42ae0..836a1baa4 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -8221,6 +8221,14 @@ "description": "The unique, numeric id of this team.", "type": "integer" }, + "include_public": { + "description": "Query parameter controlling whether to include public projects or not", + "type": "boolean" + }, + "is_public": { + "description": "Defines wether the team should be publicly discoverable when sharing a project", + "type": "boolean" + }, "members": { "description": "An array of all members in this team.", "type": "array", @@ -8356,6 +8364,14 @@ "description": "The unique, numeric id of this team.", "type": "integer" }, + "include_public": { + "description": "Query parameter controlling whether to include public projects or not", + "type": "boolean" + }, + "is_public": { + "description": "Defines wether the team should be publicly discoverable when sharing a project", + "type": "boolean" + }, "members": { "description": "An array of all members in this team.", "type": "array", @@ -8878,6 +8894,9 @@ "motd": { "type": "string" }, + "public_teams_enabled": { + "type": "boolean" + }, "registration_enabled": { "type": "boolean" }, diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 76a301bc3..1365bd24d 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -877,6 +877,14 @@ definitions: id: description: The unique, numeric id of this team. type: integer + include_public: + description: Query parameter controlling whether to include public projects + or not + type: boolean + is_public: + description: Defines wether the team should be publicly discoverable when + sharing a project + type: boolean members: description: An array of all members in this team. items: @@ -984,6 +992,14 @@ definitions: id: description: The unique, numeric id of this team. type: integer + include_public: + description: Query parameter controlling whether to include public projects + or not + type: boolean + is_public: + description: Defines wether the team should be publicly discoverable when + sharing a project + type: boolean members: description: An array of all members in this team. items: @@ -1369,6 +1385,8 @@ definitions: type: string motd: type: string + public_teams_enabled: + type: boolean registration_enabled: type: boolean task_attachments_enabled: From 4b4a7f3c0afa2344e72e695918fdad20d97ddb2d Mon Sep 17 00:00:00 2001 From: Elscrux Date: Sun, 10 Mar 2024 14:12:00 +0000 Subject: [PATCH 02/66] docs: fix broken link in migration docs (#2185) Seems like one link was broken, this attempts to fix that. Co-authored-by: Elscrux Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2185 Reviewed-by: konrad Co-authored-by: Elscrux Co-committed-by: Elscrux --- docs/content/doc/setup/migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/doc/setup/migration.md b/docs/content/doc/setup/migration.md index 1e4bececf..c364d71d6 100644 --- a/docs/content/doc/setup/migration.md +++ b/docs/content/doc/setup/migration.md @@ -14,7 +14,7 @@ menu: There are several importers available for third-party services like Trello, Microsoft To Do or Todoist. All available migration options can be found [here](https://kolaente.dev/vikunja/vikunja/src/branch/main/config.yml.sample#L218). -You can develop migrations for more services, see the [documentation]({{< ref "./development/migration.md">}}) for more info. +You can develop migrations for more services, see the [documentation]({{< ref "../development/migration.md">}}) for more info. {{< table_of_contents >}} From 4bb1d5edfca38da086b51897e1bb0bf0ebd66668 Mon Sep 17 00:00:00 2001 From: waza-ari Date: Sun, 10 Mar 2024 14:43:04 +0000 Subject: [PATCH 03/66] fix(docs): openid docs whitespace formatting (#2186) Co-authored-by: Daniel Herrmann Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2186 Reviewed-by: konrad Co-authored-by: waza-ari Co-committed-by: waza-ari --- docs/content/doc/setup/openid.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/content/doc/setup/openid.md b/docs/content/doc/setup/openid.md index b0e66e69c..4b65b2c08 100644 --- a/docs/content/doc/setup/openid.md +++ b/docs/content/doc/setup/openid.md @@ -64,7 +64,7 @@ auth: redirecturl: https://vikunja.mydomain.com/auth/openid/ <---- slash at the end is important providers: - name: - authurl: <----- Used for OIDC Discovery, usually the issuer + authurl: <----- Used for OIDC Discovery, usually the issuer clientid: clientsecret: scope: openid profile email @@ -116,7 +116,7 @@ The minimal claim structure expected by Vikunja is as follows: } ``` -It also also possible to pass the description and isPublic flag as optional parameter. If not present, the description will be empty and project visibility defaults to false. +It is also possible to pass the `description` and the `isPublic` flag as optional parameters. If not present, the description will be empty and project visibility defaults to false. ```json { @@ -124,8 +124,8 @@ It also also possible to pass the description and isPublic flag as optional para { "name": "team 3", "oidcID": 33349, - "description": "My Team Description", - "isPublic": true + "description": "My Team Description", + "isPublic": true }, ] } From ca0de680ad058e338d2081e2c2d77f864a0dc2fe Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 10 Mar 2024 16:30:01 +0100 Subject: [PATCH 04/66] fix(migration): import card covers when migrating from Trello --- go.mod | 2 ++ go.sum | 4 +-- .../migration/create_from_structure.go | 10 ++++++ pkg/modules/migration/trello/trello.go | 35 +++++++++++++++++-- pkg/modules/migration/trello/trello_test.go | 1 + 5 files changed, 48 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 33d34fa71..bcb56100a 100644 --- a/go.mod +++ b/go.mod @@ -190,3 +190,5 @@ replace github.com/samedi/caldav-go => github.com/kolaente/caldav-go v3.0.1-0.20 go 1.21 toolchain go1.21.2 + +replace github.com/adlio/trello => github.com/kolaente/trello v1.8.1-0.20240310152004-14ccae2ddc51 diff --git a/go.sum b/go.sum index 9cf20eabf..5e6e8ac31 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,6 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/ThreeDotsLabs/watermill v1.3.5 h1:50JEPEhMGZQMh08ct0tfO1PsgMOAOhV3zxK2WofkbXg= github.com/ThreeDotsLabs/watermill v1.3.5/go.mod h1:O/u/Ptyrk5MPTxSeWM5vzTtZcZfxXfO9PK9eXTYiFZY= -github.com/adlio/trello v1.10.0 h1:ia/rzoBwJJKr4IqnMlrU6n09CVqeyaahSkEVcV5/gPc= -github.com/adlio/trello v1.10.0/go.mod h1:I4Lti4jf2KxjTNgTqs5W3lLuE78QZZdYbbPnQQGwjOo= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= @@ -301,6 +299,8 @@ github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZY github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible h1:q7DbyV+sFjEoTuuUdRDNl2nlyfztkZgxVVCV7JhzIkY= github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible/go.mod h1:y1UhTNI4g0hVymJrI6yJ5/ohy09hNBeU8iJEZjgdDOw= +github.com/kolaente/trello v1.8.1-0.20240310152004-14ccae2ddc51 h1:R8xiJ/zSWOndiUjG03GmkkIm1O8MDKt2av0SeaIZy/c= +github.com/kolaente/trello v1.8.1-0.20240310152004-14ccae2ddc51/go.mod h1:I4Lti4jf2KxjTNgTqs5W3lLuE78QZZdYbbPnQQGwjOo= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= diff --git a/pkg/modules/migration/create_from_structure.go b/pkg/modules/migration/create_from_structure.go index 2fc965e9f..66bdeefa1 100644 --- a/pkg/modules/migration/create_from_structure.go +++ b/pkg/modules/migration/create_from_structure.go @@ -267,6 +267,8 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas for _, a := range t.Attachments { // Check if we have a file to create if len(a.File.FileContent) > 0 { + oldID := a.ID + a.ID = 0 a.TaskID = t.ID fr := io.NopCloser(bytes.NewReader(a.File.FileContent)) err = a.NewAttachment(s, fr, a.File.Name, a.File.Size, user) @@ -274,6 +276,14 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas return } log.Debugf("[creating structure] Created new attachment %d", a.ID) + + if t.CoverImageAttachmentID == oldID { + t.CoverImageAttachmentID = a.ID + err = t.Update(s, user) + if err != nil { + return + } + } } } diff --git a/pkg/modules/migration/trello/trello.go b/pkg/modules/migration/trello/trello.go index 3c44000fc..f1ca9b0ea 100644 --- a/pkg/modules/migration/trello/trello.go +++ b/pkg/modules/migration/trello/trello.go @@ -319,18 +319,49 @@ func convertTrelloDataToVikunja(trelloData []*trello.Board, token string) (fullV return nil, err } - task.Attachments = append(task.Attachments, &models.TaskAttachment{ + vikunjaAttachment := &models.TaskAttachment{ File: &files.File{ Name: attachment.Name, Mime: attachment.MimeType, Size: uint64(buf.Len()), FileContent: buf.Bytes(), }, - }) + } + + if card.IDAttachmentCover != "" && card.IDAttachmentCover == attachment.ID { + vikunjaAttachment.ID = 42 + task.CoverImageAttachmentID = 42 + } + + task.Attachments = append(task.Attachments, vikunjaAttachment) log.Debugf("[Trello Migration] Downloaded card attachment %s", attachment.ID) } + // When the cover image was set manually, we need to add it as an attachment + if card.ManualCoverAttachment && len(card.Cover.Scaled) > 0 { + + cover := card.Cover.Scaled[len(card.Cover.Scaled)-1] + + buf, err := migration.DownloadFile(cover.URL) + if err != nil { + return nil, err + } + + coverAttachment := &models.TaskAttachment{ + ID: 43, + File: &files.File{ + Name: cover.ID + ".jpg", + Mime: "image/jpg", // Seems to always return jpg + Size: uint64(buf.Len()), + FileContent: buf.Bytes(), + }, + } + + task.Attachments = append(task.Attachments, coverAttachment) + task.CoverImageAttachmentID = coverAttachment.ID + } + project.Tasks = append(project.Tasks, &models.TaskWithComments{Task: *task}) } diff --git a/pkg/modules/migration/trello/trello_test.go b/pkg/modules/migration/trello/trello_test.go index cb1c59c10..cc7757ca3 100644 --- a/pkg/modules/migration/trello/trello_test.go +++ b/pkg/modules/migration/trello/trello_test.go @@ -69,6 +69,7 @@ func TestConvertTrelloToVikunja(t *testing.T) { }, Attachments: []*trello.Attachment{ { + ID: "5cc71b16f0c7a57bed3c94e9", Name: "Testimage.jpg", IsUpload: true, MimeType: "image/jpg", From 22dcedcd7db2a9eaccf430acb4d184f9164b943e Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 10 Mar 2024 18:32:15 +0100 Subject: [PATCH 05/66] fix(filter): correctly replace project title in filter query Resolves https://community.vikunja.io/t/filter-option-to-exclude-a-tag-project-etc/1523/6 --- frontend/src/components/project/partials/FilterInput.vue | 6 +++--- frontend/src/components/project/partials/filters.vue | 2 +- frontend/src/helpers/filters.ts | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/project/partials/FilterInput.vue b/frontend/src/components/project/partials/FilterInput.vue index e4221a1fc..cfb8bd92f 100644 --- a/frontend/src/components/project/partials/FilterInput.vue +++ b/frontend/src/components/project/partials/FilterInput.vue @@ -211,9 +211,9 @@ function handleFieldInput() { function autocompleteSelect(value) { filterQuery.value = filterQuery.value.substring(0, autocompleteMatchPosition.value + 1) + - (autocompleteResultType.value === 'labels' - ? value.title - : value.username) + + (autocompleteResultType.value === 'assignees' + ? value.username + : value.title) + filterQuery.value.substring(autocompleteMatchPosition.value + autocompleteMatchText.value.length + 1) autocompleteResults.value = [] diff --git a/frontend/src/components/project/partials/filters.vue b/frontend/src/components/project/partials/filters.vue index 7b5b87418..85da6412c 100644 --- a/frontend/src/components/project/partials/filters.vue +++ b/frontend/src/components/project/partials/filters.vue @@ -88,7 +88,7 @@ watchDebounced( val.filter = transformFilterStringFromApi( val?.filter || '', labelId => labelStore.getLabelById(labelId)?.title, - projectId => projectStore.projects.value[projectId]?.title || null, + projectId => projectStore.projects[projectId]?.title || null, ) params.value = val }, diff --git a/frontend/src/helpers/filters.ts b/frontend/src/helpers/filters.ts index d8e4b2bd5..87d2b7263 100644 --- a/frontend/src/helpers/filters.ts +++ b/frontend/src/helpers/filters.ts @@ -33,6 +33,7 @@ export const AVAILABLE_FILTER_FIELDS = [ ...DATE_FIELDS, ...ASSIGNEE_FIELDS, ...LABEL_FIELDS, + ...PROJECT_FIELDS, ] export const FILTER_OPERATORS = [ From 0057ac5836afcbbd62fcba85d3781592d14246f6 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 10 Mar 2024 18:41:37 +0100 Subject: [PATCH 06/66] fix(migration): only download uploaded attachments --- pkg/modules/migration/trello/trello.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/modules/migration/trello/trello.go b/pkg/modules/migration/trello/trello.go index f1ca9b0ea..380ec539c 100644 --- a/pkg/modules/migration/trello/trello.go +++ b/pkg/modules/migration/trello/trello.go @@ -305,7 +305,7 @@ func convertTrelloDataToVikunja(trelloData []*trello.Board, token string) (fullV log.Debugf("[Trello Migration] Downloading %d card attachments from card %s", len(card.Attachments), card.ID) } for _, attachment := range card.Attachments { - if attachment.MimeType == "" { // Attachments can also be not downloadable - the mime type is empty in that case. + if !attachment.IsUpload { // There are other types of attachments which are not files. We can only handle files. log.Debugf("[Trello Migration] Attachment %s does not have a mime type, not downloading", attachment.ID) continue } From 6c98052176c52f390f878c80323185d8050ab293 Mon Sep 17 00:00:00 2001 From: waza-ari Date: Sun, 10 Mar 2024 21:42:34 +0000 Subject: [PATCH 07/66] fix(teams): fix duplicate teams being shown when new public team visibility feature is enabled (#2187) Due to the `INNER JOIN` on the `team_members` table and the new `OR` conditions allowing teams with the `isPublic` flag set to `true`, teams are returned multiple times. As we're only after the teams, a simple distinct query should fix the issue. Co-authored-by: Daniel Herrmann Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2187 Co-authored-by: waza-ari Co-committed-by: waza-ari --- pkg/models/teams.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/models/teams.go b/pkg/models/teams.go index 8cae52324..2c911b42e 100644 --- a/pkg/models/teams.go +++ b/pkg/models/teams.go @@ -296,7 +296,7 @@ func (t *Team) ReadAll(s *xorm.Session, a web.Auth, search string, page int, per limit, start := getLimitFromPageIndex(page, perPage) all := []*Team{} - query := s.Select("teams.*"). + query := s.Distinct("teams.*"). Table("teams"). Join("INNER", "team_members", "team_members.team_id = teams.id"). Where(db.ILIKE("teams.name", search)) From 12fbde8e8494dd2ed3a8a622d81111cce071d7ab Mon Sep 17 00:00:00 2001 From: renovate Date: Mon, 11 Mar 2024 05:06:04 +0000 Subject: [PATCH 08/66] chore(deps): update dev-dependencies --- frontend/package.json | 4 ++-- frontend/pnpm-lock.yaml | 26 +++++++++++++------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 38c5eefac..9f247aecc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -153,14 +153,14 @@ "@vue/tsconfig": "0.5.1", "autoprefixer": "10.4.18", "browserslist": "4.23.0", - "caniuse-lite": "1.0.30001596", + "caniuse-lite": "1.0.30001597", "css-has-pseudo": "6.0.2", "csstype": "3.1.3", "cypress": "13.6.6", "esbuild": "0.20.1", "eslint": "8.57.0", "eslint-plugin-vue": "9.22.0", - "happy-dom": "13.7.1", + "happy-dom": "13.7.3", "histoire": "0.17.9", "postcss": "8.4.35", "postcss-easing-gradients": "3.0.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index eebce6ce2..31ddb804f 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -313,8 +313,8 @@ devDependencies: specifier: 4.23.0 version: 4.23.0 caniuse-lite: - specifier: 1.0.30001596 - version: 1.0.30001596 + specifier: 1.0.30001597 + version: 1.0.30001597 css-has-pseudo: specifier: 6.0.2 version: 6.0.2(postcss@8.4.35) @@ -334,8 +334,8 @@ devDependencies: specifier: 9.22.0 version: 9.22.0(eslint@8.57.0) happy-dom: - specifier: 13.7.1 - version: 13.7.1 + specifier: 13.7.3 + version: 13.7.3 histoire: specifier: 0.17.9 version: 0.17.9(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0)(vite@5.1.5) @@ -386,7 +386,7 @@ devDependencies: version: 5.1.0(vue@3.4.21) vitest: specifier: 1.3.1 - version: 1.3.1(@types/node@20.11.25)(happy-dom@13.7.1)(sass@1.71.1)(terser@5.24.0) + version: 1.3.1(@types/node@20.11.25)(happy-dom@13.7.3)(sass@1.71.1)(terser@5.24.0) vue-tsc: specifier: 2.0.6 version: 2.0.6(typescript@5.4.2) @@ -4548,7 +4548,7 @@ packages: postcss: ^8.1.0 dependencies: browserslist: 4.23.0 - caniuse-lite: 1.0.30001596 + caniuse-lite: 1.0.30001597 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.0.0 @@ -4704,7 +4704,7 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001596 + caniuse-lite: 1.0.30001597 electron-to-chromium: 1.4.685 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.23.0) @@ -4790,8 +4790,8 @@ packages: engines: {node: '>=6'} dev: true - /caniuse-lite@1.0.30001596: - resolution: {integrity: sha512-zpkZ+kEr6We7w63ORkoJ2pOfBwBkY/bJrG/UZ90qNb45Isblu8wzDgevEOrRL1r9dWayHjYiiyCMEXPn4DweGQ==} + /caniuse-lite@1.0.30001597: + resolution: {integrity: sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==} dev: true /capital-case@1.0.4: @@ -6449,8 +6449,8 @@ packages: strip-bom-string: 1.0.0 dev: true - /happy-dom@13.7.1: - resolution: {integrity: sha512-uQgxSTqQY4lMVIhV/W6GWYOT6h7Z6CNlsa+SyvAcOy311spU3zPDNAMzayJky9q4xqfEQf3cQj8yDZngiYUEDA==} + /happy-dom@13.7.3: + resolution: {integrity: sha512-xMwilTgO34BGEX0TAoM369wwwAy0fK/Jq6BGaRYhSjxLtfQ740nqxHfjFyBqPjCVmKiPUS4npBnMrGLii7eCOg==} engines: {node: '>=16.0.0'} dependencies: entities: 4.5.0 @@ -10079,7 +10079,7 @@ packages: fsevents: 2.3.3 dev: true - /vitest@1.3.1(@types/node@20.11.25)(happy-dom@13.7.1)(sass@1.71.1)(terser@5.24.0): + /vitest@1.3.1(@types/node@20.11.25)(happy-dom@13.7.3)(sass@1.71.1)(terser@5.24.0): resolution: {integrity: sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -10114,7 +10114,7 @@ packages: chai: 4.3.10 debug: 4.3.4(supports-color@8.1.1) execa: 8.0.1 - happy-dom: 13.7.1 + happy-dom: 13.7.3 local-pkg: 0.5.0 magic-string: 0.30.7 pathe: 1.1.1 From 3b77fff4c999d2c95788da602b11e7b91189727d Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 11 Mar 2024 14:21:42 +0100 Subject: [PATCH 09/66] fix(project): correctly show the number of tasks and projects when deleting a project --- frontend/src/i18n/lang/en.json | 1 + .../src/views/project/settings/delete.vue | 30 ++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/frontend/src/i18n/lang/en.json b/frontend/src/i18n/lang/en.json index b7747ecb7..8516f6812 100644 --- a/frontend/src/i18n/lang/en.json +++ b/frontend/src/i18n/lang/en.json @@ -248,6 +248,7 @@ "text2": "This includes all tasks and CANNOT BE UNDONE!", "success": "The project was successfully deleted.", "tasksToDelete": "This will irrevocably remove approx. {count} tasks.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "This project does not contain any tasks, it should be safe to delete." }, "duplicate": { diff --git a/frontend/src/views/project/settings/delete.vue b/frontend/src/views/project/settings/delete.vue index 2ee998672..059099bdb 100644 --- a/frontend/src/views/project/settings/delete.vue +++ b/frontend/src/views/project/settings/delete.vue @@ -16,9 +16,7 @@ v-if="totalTasks !== null" class="has-text-weight-bold" > - {{ - totalTasks > 0 ? $t('project.delete.tasksToDelete', {count: totalTasks}) : $t('project.delete.noTasksToDelete') - }} + {{ deleteNotice }}

(null) const project = computed(() => projectStore.projects[route.params.projectId]) +const childProjectIds = ref([]) watchEffect( () => { @@ -58,15 +57,32 @@ watchEffect( return } - const taskCollectionService = new TaskCollectionService() - taskCollectionService.getAll({projectId: route.params.projectId}).then(() => { - totalTasks.value = taskCollectionService.totalPages * taskCollectionService.resultCount + childProjectIds.value = projectStore.getChildProjects(parseInt(route.params.projectId)).map(p => p.id) + if (childProjectIds.value.length === 0) { + childProjectIds.value = [parseInt(route.params.projectId)] + } + + const taskService = new TaskService() + taskService.getAll({}, {filter: `project in '${childProjectIds.value.join(',')}'`}).then(() => { + totalTasks.value = taskService.totalPages * taskService.resultCount }) }, ) useTitle(() => t('project.delete.title', {project: project?.value?.title})) +const deleteNotice = computed(() => { + if(totalTasks.value && totalTasks.value > 0 && childProjectIds.value.length <= 1) { + return t('project.delete.tasksToDelete', {count: totalTasks.value}) + } + + if(totalTasks.value && totalTasks.value > 0 && childProjectIds.value.length > 1) { + return t('project.delete.tasksAndChildProjectsToDelete', {tasks: totalTasks.value, projects: childProjectIds.value.length}) + } + + return t('project.delete.noTasksToDelete') +}) + async function deleteProject() { if (!project.value) { return From 3896c680d35382be891f48b64ce0451411336f4e Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 11 Mar 2024 14:36:59 +0100 Subject: [PATCH 10/66] fix(filters): do not require string for in comparator --- frontend/src/views/project/settings/delete.vue | 2 +- pkg/models/task_collection_filter.go | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/project/settings/delete.vue b/frontend/src/views/project/settings/delete.vue index 059099bdb..86980a942 100644 --- a/frontend/src/views/project/settings/delete.vue +++ b/frontend/src/views/project/settings/delete.vue @@ -63,7 +63,7 @@ watchEffect( } const taskService = new TaskService() - taskService.getAll({}, {filter: `project in '${childProjectIds.value.join(',')}'`}).then(() => { + taskService.getAll({}, {filter: `project in ${childProjectIds.value.join(',')}`}).then(() => { totalTasks.value = taskService.totalPages * taskService.resultCount }) }, diff --git a/pkg/models/task_collection_filter.go b/pkg/models/task_collection_filter.go index 0e69fb2f9..3195fa4df 100644 --- a/pkg/models/task_collection_filter.go +++ b/pkg/models/task_collection_filter.go @@ -19,6 +19,7 @@ package models import ( "fmt" "reflect" + "regexp" "strconv" "strings" "time" @@ -153,6 +154,18 @@ func getTaskFiltersFromFilterString(filter string) (filters []*taskFilter, err e filter = strings.ReplaceAll(filter, " in ", " ?= ") + // Replaces all occurences with in with a string so that it passes the filter + pattern := `\?=\s+([^&|]+)` + re := regexp.MustCompile(pattern) + + filter = re.ReplaceAllStringFunc(filter, func(match string) string { + value := strings.TrimSpace(strings.TrimPrefix(match, "?=")) + value = strings.ReplaceAll(value, "'", `\'`) + enclosedValue := "'" + value + "'" + + return "?= " + enclosedValue + }) + parsedFilter, err := fexpr.Parse(filter) if err != nil { return nil, &ErrInvalidFilterExpression{ @@ -242,7 +255,7 @@ func getValueForField(field reflect.StructField, rawValue string) (value interfa // In that case we don't really care about what the actual type is, we just cast the value to an // int64 since we need the id - yes, this assumes we only ever have int64 IDs, but this is fine. if field.Type.Elem().Kind() == reflect.Ptr { - value, err = strconv.ParseInt(rawValue, 10, 64) + value, err = strconv.ParseInt(strings.TrimSpace(rawValue), 10, 64) return } From 0529f30e770416e18ef575f66bd1be8aeeb18678 Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 11 Mar 2024 15:16:39 +0100 Subject: [PATCH 11/66] fix(filters): parse labels and projects correctly when using `in` filter operator --- frontend/src/helpers/filters.test.ts | 118 ++++++++++++++++----------- frontend/src/helpers/filters.ts | 64 +++++++++++---- 2 files changed, 118 insertions(+), 64 deletions(-) diff --git a/frontend/src/helpers/filters.test.ts b/frontend/src/helpers/filters.test.ts index a1910ff26..6f04f5757 100644 --- a/frontend/src/helpers/filters.test.ts +++ b/frontend/src/helpers/filters.test.ts @@ -17,7 +17,7 @@ describe('Filter Transformation', () => { 'assignees': 'assignees', 'labels': 'labels', } - + describe('For api', () => { for (const c in fieldCases) { it('should transform all filter params for ' + c + ' to snake_case', () => { @@ -37,23 +37,35 @@ describe('Filter Transformation', () => { expect(transformed).toBe('labels = 1') }) + const multipleDummyResolver = (title: string) => { + switch (title) { + case 'lorem': + return 1 + case 'ipsum': + return 2 + default: + return null + } + } + it('should correctly resolve multiple labels', () => { const transformed = transformFilterStringForApi( 'labels = lorem && dueDate = now && labels = ipsum', - (title: string) => { - switch (title) { - case 'lorem': - return 1 - case 'ipsum': - return 2 - default: - return null - } - }, + multipleDummyResolver, nullTitleToIdResolver, ) - expect(transformed).toBe('labels = 1&& due_date = now && labels = 2') + expect(transformed).toBe('labels = 1 && due_date = now && labels = 2') + }) + + it('should correctly resolve multiple labels with an in clause', () => { + const transformed = transformFilterStringForApi( + 'labels in lorem, ipsum && dueDate = now', + multipleDummyResolver, + nullTitleToIdResolver, + ) + + expect(transformed).toBe('labels in 1, 2 && due_date = now') }) it('should correctly resolve projects', () => { @@ -70,19 +82,20 @@ describe('Filter Transformation', () => { const transformed = transformFilterStringForApi( 'project = lorem && dueDate = now || project = ipsum', nullTitleToIdResolver, - (title: string) => { - switch (title) { - case 'lorem': - return 1 - case 'ipsum': - return 2 - default: - return null - } - }, + multipleDummyResolver, ) - expect(transformed).toBe('project = 1&& due_date = now || project = 2') + expect(transformed).toBe('project = 1 && due_date = now || project = 2') + }) + + it('should correctly resolve multiple projects with in', () => { + const transformed = transformFilterStringForApi( + 'project in lorem, ipsum', + nullTitleToIdResolver, + multipleDummyResolver, + ) + + expect(transformed).toBe('project in 1, 2') }) }) @@ -104,24 +117,36 @@ describe('Filter Transformation', () => { expect(transformed).toBe('labels = lorem') }) - + + const multipleIdToTitleResolver = (id: number) => { + switch (id) { + case 1: + return 'lorem' + case 2: + return 'ipsum' + default: + return null + } + } + it('should correctly resolve multiple labels', () => { const transformed = transformFilterStringFromApi( 'labels = 1 && due_date = now && labels = 2', - (id: number) => { - switch (id) { - case 1: - return 'lorem' - case 2: - return 'ipsum' - default: - return null - } - }, + multipleIdToTitleResolver, nullIdToTitleResolver, ) - expect(transformed).toBe('labels = lorem&& dueDate = now && labels = ipsum') + expect(transformed).toBe('labels = lorem && dueDate = now && labels = ipsum') + }) + + it('should correctly resolve multiple labels in', () => { + const transformed = transformFilterStringFromApi( + 'labels in 1, 2', + multipleIdToTitleResolver, + nullIdToTitleResolver, + ) + + expect(transformed).toBe('labels in lorem, ipsum') }) it('should correctly resolve projects', () => { @@ -136,21 +161,22 @@ describe('Filter Transformation', () => { it('should correctly resolve multiple projects', () => { const transformed = transformFilterStringFromApi( - 'project = lorem && due_date = now || project = ipsum', + 'project = 1 && due_date = now || project = 2', nullIdToTitleResolver, - (id: number) => { - switch (id) { - case 1: - return 'lorem' - case 2: - return 'ipsum' - default: - return null - } - }, + multipleIdToTitleResolver, ) expect(transformed).toBe('project = lorem && dueDate = now || project = ipsum') }) + + it('should correctly resolve multiple projects in', () => { + const transformed = transformFilterStringFromApi( + 'project in 1, 2', + nullIdToTitleResolver, + multipleIdToTitleResolver, + ) + + expect(transformed).toBe('project in lorem, ipsum') + }) }) }) diff --git a/frontend/src/helpers/filters.ts b/frontend/src/helpers/filters.ts index 87d2b7263..9f720e5d9 100644 --- a/frontend/src/helpers/filters.ts +++ b/frontend/src/helpers/filters.ts @@ -55,7 +55,7 @@ export const FILTER_JOIN_OPERATOR = [ ')', ] -export const FILTER_OPERATORS_REGEX = '(<|>|<=|>=|=|!=)' +export const FILTER_OPERATORS_REGEX = '(<|>|<=|>=|=|!=|in)' function getFieldPattern(field: string): RegExp { return new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&|()]+\\1?)?', 'ig') @@ -66,11 +66,11 @@ export function transformFilterStringForApi( labelResolver: (title: string) => number | null, projectResolver: (title: string) => number | null, ): string { - + if (filter.trim() === '') { return '' } - + // Transform labels to ids LABEL_FIELDS.forEach(field => { const pattern = getFieldPattern(field) @@ -80,10 +80,17 @@ export function transformFilterStringForApi( // eslint-disable-next-line @typescript-eslint/no-unused-vars const [matched, prefix, operator, space, keyword] = match if (keyword) { - const labelId = labelResolver(keyword.trim()) - if (labelId !== null) { - filter = filter.replace(keyword, String(labelId)) + let keywords = [keyword.trim()] + if (operator === 'in' || operator === '?=') { + keywords = keyword.trim().split(',').map(k => k.trim()) } + + keywords.forEach(k => { + const labelId = labelResolver(k) + if (labelId !== null) { + filter = filter.replace(k, String(labelId)) + } + }) } } }) @@ -96,10 +103,17 @@ export function transformFilterStringForApi( // eslint-disable-next-line @typescript-eslint/no-unused-vars const [matched, prefix, operator, space, keyword] = match if (keyword) { - const projectId = projectResolver(keyword.trim()) - if (projectId !== null) { - filter = filter.replace(keyword, String(projectId)) + let keywords = [keyword.trim()] + if (operator === 'in' || operator === '?=') { + keywords = keyword.trim().split(',').map(k => k.trim()) } + + keywords.forEach(k => { + const projectId = projectResolver(k) + if (projectId !== null) { + filter = filter.replace(k, String(projectId)) + } + }) } } }) @@ -117,16 +131,16 @@ export function transformFilterStringFromApi( labelResolver: (id: number) => string | null, projectResolver: (id: number) => string | null, ): string { - + if (filter.trim() === '') { return '' } - + // Transform all attributes from snake case AVAILABLE_FILTER_FIELDS.forEach(f => { filter = filter.replace(snakeCase(f), f) }) - + // Transform labels to their titles LABEL_FIELDS.forEach(field => { const pattern = getFieldPattern(field) @@ -136,10 +150,17 @@ export function transformFilterStringFromApi( // eslint-disable-next-line @typescript-eslint/no-unused-vars const [matched, prefix, operator, space, keyword] = match if (keyword) { - const labelTitle = labelResolver(Number(keyword.trim())) - if (labelTitle !== null) { - filter = filter.replace(keyword, labelTitle) + let keywords = [keyword.trim()] + if (operator === 'in' || operator === '?=') { + keywords = keyword.trim().split(',').map(k => k.trim()) } + + keywords.forEach(k => { + const labelTitle = labelResolver(parseInt(k)) + if (labelTitle !== null) { + filter = filter.replace(k, labelTitle) + } + }) } } }) @@ -153,10 +174,17 @@ export function transformFilterStringFromApi( // eslint-disable-next-line @typescript-eslint/no-unused-vars const [matched, prefix, operator, space, keyword] = match if (keyword) { - const project = projectResolver(Number(keyword.trim())) - if (project !== null) { - filter = filter.replace(keyword, project) + let keywords = [keyword.trim()] + if (operator === 'in' || operator === '?=') { + keywords = keyword.trim().split(',').map(k => k.trim()) } + + keywords.forEach(k => { + const project = projectResolver(parseInt(k)) + if (project !== null) { + filter = filter.replace(k, project) + } + }) } } }) From dbfe162cd233b8ddd16317ac065a54e6c393afb3 Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 11 Mar 2024 15:41:06 +0100 Subject: [PATCH 12/66] fix(filters): label highlighting and autocomplete fields now work with in operator Previously, when creating a filter query with the 'in' operator and multiple values, autocompletion and highlighting was not available. This change now implements a split for each value, seperated by a comma. --- docs/content/doc/usage/filters.md | 2 +- .../project/partials/FilterInput.vue | 35 +++++++++++++------ frontend/src/helpers/filters.ts | 10 +++--- frontend/src/i18n/lang/en.json | 2 +- frontend/src/stores/labels.ts | 7 ++++ 5 files changed, 39 insertions(+), 17 deletions(-) diff --git a/docs/content/doc/usage/filters.md b/docs/content/doc/usage/filters.md index b7885efc9..2a841469a 100644 --- a/docs/content/doc/usage/filters.md +++ b/docs/content/doc/usage/filters.md @@ -46,7 +46,7 @@ The available operators for filtering include: * `<`: Less than * `<=`: Less than or equal to * `like`: Matches a pattern (using wildcard `%`) -* `in`: Matches any value in a list +* `in`: Matches any value in a comma-seperated list of values To combine multiple conditions, you can use the following logical operators: diff --git a/frontend/src/components/project/partials/FilterInput.vue b/frontend/src/components/project/partials/FilterInput.vue index cfb8bd92f..e02eb6838 100644 --- a/frontend/src/components/project/partials/FilterInput.vue +++ b/frontend/src/components/project/partials/FilterInput.vue @@ -16,7 +16,7 @@ import { AVAILABLE_FILTER_FIELDS, FILTER_JOIN_OPERATOR, FILTER_OPERATORS, - FILTER_OPERATORS_REGEX, LABEL_FIELDS, + FILTER_OPERATORS_REGEX, LABEL_FIELDS, getFilterFieldRegexPattern, } from '@/helpers/filters' const { @@ -104,15 +104,25 @@ const highlightedFilterQuery = computed(() => { }) LABEL_FIELDS .forEach(f => { - const pattern = new RegExp(f + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*([\'"]?)([^\'"\\s]+\\1?)?', 'ig') - highlighted = highlighted.replaceAll(pattern, (match, token, start, value) => { + const pattern = getFilterFieldRegexPattern(f) + highlighted = highlighted.replaceAll(pattern, (match, prefix, operator, space, value) => { + if (typeof value === 'undefined') { value = '' } + + let labelTitles = [value] + if(operator === 'in' || operator === '?=') { + labelTitles = value.split(',').map(v => v.trim()) + } - const label = labelStore.getLabelsByExactTitles([value])[0] || undefined + const labelsHtml: string[] = [] + labelTitles.forEach(t => { + const label = labelStore.getLabelByExactTitle(t) || undefined + labelsHtml.push(`${label?.title ?? t}`) + }) - return `${f} ${token} ${label?.title ?? value}` + return `${f} ${operator} ${labelsHtml.join(', ')}` }) }) FILTER_OPERATORS @@ -184,26 +194,31 @@ function handleFieldInput() { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [matched, prefix, operator, space, keyword] = match if (keyword) { + let search = keyword + if(operator === 'in' || operator === '?=') { + const keywords = keyword.split(',') + search = keywords[keywords.length - 1].trim() + } if (matched.startsWith('label')) { autocompleteResultType.value = 'labels' - autocompleteResults.value = labelStore.filterLabelsByQuery([], keyword) + autocompleteResults.value = labelStore.filterLabelsByQuery([], search) } if (matched.startsWith('assignee')) { autocompleteResultType.value = 'assignees' if (projectId) { - projectUserService.getAll({projectId}, {s: keyword}) + projectUserService.getAll({projectId}, {s: search}) .then(users => autocompleteResults.value = users.length > 1 ? users : []) } else { - userService.getAll({}, {s: keyword}) + userService.getAll({}, {s: search}) .then(users => autocompleteResults.value = users.length > 1 ? users : []) } } if (!projectId && matched.startsWith('project')) { autocompleteResultType.value = 'projects' - autocompleteResults.value = projectStore.searchProject(keyword) + autocompleteResults.value = projectStore.searchProject(search) } autocompleteMatchText.value = keyword - autocompleteMatchPosition.value = prefix.length - 1 + autocompleteMatchPosition.value = prefix.length - 1 + keyword.replace(search, '').length } } }) diff --git a/frontend/src/helpers/filters.ts b/frontend/src/helpers/filters.ts index 9f720e5d9..de10f57f6 100644 --- a/frontend/src/helpers/filters.ts +++ b/frontend/src/helpers/filters.ts @@ -57,7 +57,7 @@ export const FILTER_JOIN_OPERATOR = [ export const FILTER_OPERATORS_REGEX = '(<|>|<=|>=|=|!=|in)' -function getFieldPattern(field: string): RegExp { +export function getFilterFieldRegexPattern(field: string): RegExp { return new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&|()]+\\1?)?', 'ig') } @@ -73,7 +73,7 @@ export function transformFilterStringForApi( // Transform labels to ids LABEL_FIELDS.forEach(field => { - const pattern = getFieldPattern(field) + const pattern = getFilterFieldRegexPattern(field) let match: RegExpExecArray | null while ((match = pattern.exec(filter)) !== null) { @@ -96,7 +96,7 @@ export function transformFilterStringForApi( }) // Transform projects to ids PROJECT_FIELDS.forEach(field => { - const pattern = getFieldPattern(field) + const pattern = getFilterFieldRegexPattern(field) let match: RegExpExecArray | null while ((match = pattern.exec(filter)) !== null) { @@ -143,7 +143,7 @@ export function transformFilterStringFromApi( // Transform labels to their titles LABEL_FIELDS.forEach(field => { - const pattern = getFieldPattern(field) + const pattern = getFilterFieldRegexPattern(field) let match: RegExpExecArray | null while ((match = pattern.exec(filter)) !== null) { @@ -167,7 +167,7 @@ export function transformFilterStringFromApi( // Transform projects to ids PROJECT_FIELDS.forEach(field => { - const pattern = getFieldPattern(field) + const pattern = getFilterFieldRegexPattern(field) let match: RegExpExecArray | null while ((match = pattern.exec(filter)) !== null) { diff --git a/frontend/src/i18n/lang/en.json b/frontend/src/i18n/lang/en.json index 8516f6812..158ef586f 100644 --- a/frontend/src/i18n/lang/en.json +++ b/frontend/src/i18n/lang/en.json @@ -446,7 +446,7 @@ "lessThan": "Less than", "lessThanOrEqual": "Less than or equal to", "like": "Matches a pattern (using wildcard %)", - "in": "Matches any value in a list" + "in": "Matches any value in a comma-seperated list of values" }, "logicalOperators": { "intro": "To combine multiple conditions, you can use the following logical operators:", diff --git a/frontend/src/stores/labels.ts b/frontend/src/stores/labels.ts index 5d8c48221..9c3d959b0 100644 --- a/frontend/src/stores/labels.ts +++ b/frontend/src/stores/labels.ts @@ -57,6 +57,12 @@ export const useLabelStore = defineStore('label', () => { .values(labels.value) .filter(({title}) => labelTitles.some(l => l.toLowerCase() === title.toLowerCase())) }) + + const getLabelByExactTitle = computed(() => { + return (labelTitle: string) => Object + .values(labels.value) + .find(l => l.title.toLowerCase() === labelTitle.toLowerCase()) + }) function setIsLoading(newIsLoading: boolean) { @@ -145,6 +151,7 @@ export const useLabelStore = defineStore('label', () => { getLabelById, filterLabelsByQuery, getLabelsByExactTitles, + getLabelByExactTitle, setLabels, setLabel, From 6fc3d1e98fe28d7e561a4ebe1d00938f8346fae1 Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 11 Mar 2024 15:42:09 +0100 Subject: [PATCH 13/66] fix: lint --- pkg/models/task_collection_filter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/models/task_collection_filter.go b/pkg/models/task_collection_filter.go index 3195fa4df..560094bf9 100644 --- a/pkg/models/task_collection_filter.go +++ b/pkg/models/task_collection_filter.go @@ -154,7 +154,7 @@ func getTaskFiltersFromFilterString(filter string) (filters []*taskFilter, err e filter = strings.ReplaceAll(filter, " in ", " ?= ") - // Replaces all occurences with in with a string so that it passes the filter + // Replaces all occurrences with in with a string so that it passes the filter pattern := `\?=\s+([^&|]+)` re := regexp.MustCompile(pattern) From a66e26678ece858cc13515e160acbd5261371dbc Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 11 Mar 2024 16:13:42 +0100 Subject: [PATCH 14/66] feat(filters): pass timezone down when filtering with relative date math Resolves https://community.vikunja.io/t/my-vikunja-instance-creates-tasks-with-due-date-time-of-9am-for-tasks-with-the-word-today-word-in-it/2105/8 --- frontend/src/composables/useTaskList.ts | 10 ++++-- frontend/src/services/taskCollection.ts | 2 ++ frontend/src/stores/kanban.ts | 11 +++++-- frontend/src/stores/tasks.ts | 8 +++-- .../views/project/helpers/useGanttTaskList.ts | 7 ++++ frontend/src/views/tasks/ShowTasks.vue | 2 ++ pkg/models/kanban.go | 3 +- pkg/models/task_collection.go | 7 +++- pkg/models/task_collection_filter.go | 33 +++++++++++++------ pkg/models/tasks.go | 1 + 10 files changed, 65 insertions(+), 19 deletions(-) diff --git a/frontend/src/composables/useTaskList.ts b/frontend/src/composables/useTaskList.ts index 51e874c4d..7b05375e8 100644 --- a/frontend/src/composables/useTaskList.ts +++ b/frontend/src/composables/useTaskList.ts @@ -6,6 +6,7 @@ import TaskCollectionService, {getDefaultTaskFilterParams} from '@/services/task import type {ITask} from '@/modelTypes/ITask' import {error} from '@/message' import type {IProject} from '@/modelTypes/IProject' +import {useAuthStore} from '@/stores/auth' export type Order = 'asc' | 'desc' | 'none' @@ -81,11 +82,16 @@ export function useTaskList(projectIdGetter: ComputedGetter, sor page.value = 1 }, ) - + + const authStore = useAuthStore() + const getAllTasksParams = computed(() => { return [ {projectId: projectId.value}, - allParams.value, + { + ...allParams.value, + filter_timezone: authStore.settings.timezone, + }, page.value, ] }) diff --git a/frontend/src/services/taskCollection.ts b/frontend/src/services/taskCollection.ts index 0c8a13f06..965ad05b3 100644 --- a/frontend/src/services/taskCollection.ts +++ b/frontend/src/services/taskCollection.ts @@ -8,6 +8,7 @@ export interface TaskFilterParams { order_by: ('asc' | 'desc')[], filter: string, filter_include_nulls: boolean, + filter_timezone: string, s: string, } @@ -17,6 +18,7 @@ export function getDefaultTaskFilterParams(): TaskFilterParams { order_by: ['asc', 'desc'], filter: '', filter_include_nulls: false, + filter_timezone: '', s: '', } } diff --git a/frontend/src/stores/kanban.ts b/frontend/src/stores/kanban.ts index 94c626748..6c0e7b044 100644 --- a/frontend/src/stores/kanban.ts +++ b/frontend/src/stores/kanban.ts @@ -7,13 +7,14 @@ import {i18n} from '@/i18n' import {success} from '@/message' import BucketService from '@/services/bucket' -import TaskCollectionService from '@/services/taskCollection' +import TaskCollectionService, {type TaskFilterParams} from '@/services/taskCollection' import {setModuleLoading} from '@/stores/helper' import type {ITask} from '@/modelTypes/ITask' import type {IProject} from '@/modelTypes/IProject' import type {IBucket} from '@/modelTypes/IBucket' +import {useAuthStore} from '@/stores/auth' const TASKS_PER_BUCKET = 25 @@ -44,6 +45,8 @@ const addTaskToBucketAndSort = (buckets: IBucket[], task: ITask) => { * It should hold only the current buckets. */ export const useKanbanStore = defineStore('kanban', () => { + const authStore = useAuthStore() + const buckets = ref([]) const projectId = ref(0) const bucketLoading = ref<{[id: IBucket['id']]: boolean}>({}) @@ -247,7 +250,7 @@ export const useKanbanStore = defineStore('kanban', () => { async function loadNextTasksForBucket( projectId: IProject['id'], - ps, + ps: TaskFilterParams, bucketId: IBucket['id'], ) { const isLoading = bucketLoading.value[bucketId] ?? false @@ -265,7 +268,7 @@ export const useKanbanStore = defineStore('kanban', () => { const cancel = setModuleLoading(setIsLoading) setBucketLoading({bucketId: bucketId, loading: true}) - const params = JSON.parse(JSON.stringify(ps)) + const params: TaskFilterParams = JSON.parse(JSON.stringify(ps)) params.sort_by = 'kanban_position' params.order_by = 'asc' @@ -286,6 +289,8 @@ export const useKanbanStore = defineStore('kanban', () => { params.filter_value = [...(params.filter_value ?? []), bucketId] params.filter_comparator = [...(params.filter_comparator ?? []), 'equals'] } + + params.filter_timezone = authStore.settings.timezone params.per_page = TASKS_PER_BUCKET diff --git a/frontend/src/stores/tasks.ts b/frontend/src/stores/tasks.ts index b57213766..9cf3ab3e5 100644 --- a/frontend/src/stores/tasks.ts +++ b/frontend/src/stores/tasks.ts @@ -28,7 +28,7 @@ import {useKanbanStore} from '@/stores/kanban' import {useBaseStore} from '@/stores/base' import ProjectUserService from '@/services/projectUsers' import {useAuthStore} from '@/stores/auth' -import TaskCollectionService from '@/services/taskCollection' +import TaskCollectionService, {type TaskFilterParams} from '@/services/taskCollection' import {getRandomColorHex} from '@/helpers/color/randomColor' interface MatchedAssignee extends IUser { @@ -124,7 +124,11 @@ export const useTaskStore = defineStore('task', () => { }) } - async function loadTasks(params, projectId: IProject['id'] | null = null) { + async function loadTasks(params: TaskFilterParams, projectId: IProject['id'] | null = null) { + + if (params.filter_timezone === '') { + params.filter_timezone = authStore.settings.timezone + } const cancel = setModuleLoading(setIsLoading) try { diff --git a/frontend/src/views/project/helpers/useGanttTaskList.ts b/frontend/src/views/project/helpers/useGanttTaskList.ts index 7f094252c..f2a76c8d6 100644 --- a/frontend/src/views/project/helpers/useGanttTaskList.ts +++ b/frontend/src/views/project/helpers/useGanttTaskList.ts @@ -9,6 +9,7 @@ import TaskService from '@/services/task' import TaskModel from '@/models/task' import {error, success} from '@/message' +import {useAuthStore} from '@/stores/auth' // FIXME: unify with general `useTaskList` export function useGanttTaskList( @@ -21,12 +22,18 @@ export function useGanttTaskList( }) { const taskCollectionService = shallowReactive(new TaskCollectionService()) const taskService = shallowReactive(new TaskService()) + const authStore = useAuthStore() const isLoading = computed(() => taskCollectionService.loading) const tasks = ref>(new Map()) async function fetchTasks(params: TaskFilterParams, page = 1): Promise { + + if(params.filter_timezone === '') { + params.filter_timezone = authStore.settings.timezone + } + const tasks = await taskCollectionService.getAll({projectId: filters.value.projectId}, params, page) as ITask[] if (options.loadAll && page < taskCollectionService.totalPages) { const nextTasks = await fetchTasks(params, page + 1) diff --git a/frontend/src/views/tasks/ShowTasks.vue b/frontend/src/views/tasks/ShowTasks.vue index 0d2d747ea..899edd04c 100644 --- a/frontend/src/views/tasks/ShowTasks.vue +++ b/frontend/src/views/tasks/ShowTasks.vue @@ -85,6 +85,7 @@ import type {ITask} from '@/modelTypes/ITask' import {useAuthStore} from '@/stores/auth' import {useTaskStore} from '@/stores/tasks' import {useProjectStore} from '@/stores/projects' +import type {TaskFilterParams} from '@/services/taskCollection' // Linting disabled because we explicitely enabled destructuring in vite's config, this will work. // eslint-disable-next-line vue/no-setup-props-destructure @@ -184,6 +185,7 @@ async function loadPendingTasks(from: string, to: string) { const params = { sortBy: ['due_date', 'id'], orderBy: ['asc', 'desc'], + filterTimezone: authStore.settings.timezone, filterBy: ['done'], filterValue: ['false'], filterComparator: ['equals'], diff --git a/pkg/models/kanban.go b/pkg/models/kanban.go index 24a307c44..597420a97 100644 --- a/pkg/models/kanban.go +++ b/pkg/models/kanban.go @@ -109,6 +109,7 @@ func getDefaultBucketID(s *xorm.Session, project *Project) (bucketID int64, err // @Param per_page query int false "The maximum number of tasks per bucket per page. This parameter is limited by the configured maximum of items per page." // @Param s query string false "Search tasks by task text." // @Param filter query string false "The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature." +// @Param filter_timezone query string false "The time zone which should be used for date match (statements like "now" resolve to different actual times)" // @Param filter_include_nulls query string false "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`." // @Success 200 {array} models.Bucket "The buckets with their tasks" // @Failure 500 {object} models.Message "Internal server error" @@ -197,7 +198,7 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int } else { filterString = "(" + originalFilter + ") && bucket_id = " + strconv.FormatInt(id, 10) } - opts.parsedFilters, err = getTaskFiltersFromFilterString(filterString) + opts.parsedFilters, err = getTaskFiltersFromFilterString(filterString, opts.filterTimezone) if err != nil { return } diff --git a/pkg/models/task_collection.go b/pkg/models/task_collection.go index bb844086e..4c8160b72 100644 --- a/pkg/models/task_collection.go +++ b/pkg/models/task_collection.go @@ -33,7 +33,10 @@ type TaskCollection struct { OrderBy []string `query:"order_by" json:"order_by"` OrderByArr []string `query:"order_by[]" json:"-"` + // The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature. Filter string `query:"filter" json:"filter"` + // The time zone which should be used for date match (statements like "now" resolve to different actual times) + FilterTimezone string `query:"filter_timezone" json:"filter_timezone"` // If set to true, the result will also include null values FilterIncludeNulls bool `query:"filter_include_nulls" json:"filter_include_nulls"` @@ -103,9 +106,10 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskSearchOption sortby: sort, filterIncludeNulls: tf.FilterIncludeNulls, filter: tf.Filter, + filterTimezone: tf.FilterTimezone, } - opts.parsedFilters, err = getTaskFiltersFromFilterString(tf.Filter) + opts.parsedFilters, err = getTaskFiltersFromFilterString(tf.Filter, tf.FilterTimezone) return opts, err } @@ -122,6 +126,7 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskSearchOption // @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `project_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`." // @Param order_by query string false "The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`." // @Param filter query string false "The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature." +// @Param filter_timezone query string false "The time zone which should be used for date match (statements like "now" resolve to different actual times)" // @Param filter_include_nulls query string false "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`." // @Security JWTKeyAuth // @Success 200 {array} models.Task "The tasks" diff --git a/pkg/models/task_collection_filter.go b/pkg/models/task_collection_filter.go index 560094bf9..d0885ff4d 100644 --- a/pkg/models/task_collection_filter.go +++ b/pkg/models/task_collection_filter.go @@ -92,7 +92,7 @@ func parseTimeFromUserInput(timeString string) (value time.Time, err error) { return value.In(config.GetTimeZone()), err } -func parseFilterFromExpression(f fexpr.ExprGroup) (filter *taskFilter, err error) { +func parseFilterFromExpression(f fexpr.ExprGroup, loc *time.Location) (filter *taskFilter, err error) { filter = &taskFilter{ join: filterConcatAnd, } @@ -112,7 +112,7 @@ func parseFilterFromExpression(f fexpr.ExprGroup) (filter *taskFilter, err error case []fexpr.ExprGroup: values := make([]*taskFilter, 0, len(v)) for _, expression := range v { - subfilter, err := parseFilterFromExpression(expression) + subfilter, err := parseFilterFromExpression(expression, loc) if err != nil { return nil, err } @@ -132,7 +132,7 @@ func parseFilterFromExpression(f fexpr.ExprGroup) (filter *taskFilter, err error if filter.field == "project" { filter.field = "project_id" } - reflectValue, filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, value) + reflectValue, filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, value, loc) if err != nil { return nil, ErrInvalidTaskFilterValue{ Value: filter.field, @@ -146,7 +146,7 @@ func parseFilterFromExpression(f fexpr.ExprGroup) (filter *taskFilter, err error return filter, nil } -func getTaskFiltersFromFilterString(filter string) (filters []*taskFilter, err error) { +func getTaskFiltersFromFilterString(filter string, filterTimezone string) (filters []*taskFilter, err error) { if filter == "" { return @@ -174,9 +174,17 @@ func getTaskFiltersFromFilterString(filter string) (filters []*taskFilter, err e } } + var loc *time.Location + if filterTimezone != "" { + loc, err = time.LoadLocation(filterTimezone) + if err != nil { + return + } + } + filters = make([]*taskFilter, 0, len(parsedFilter)) for _, f := range parsedFilter { - parsedFilter, err := parseFilterFromExpression(f) + parsedFilter, err := parseFilterFromExpression(f, loc) if err != nil { return nil, err } @@ -230,7 +238,12 @@ func getFilterComparatorFromOp(op fexpr.SignOp) (taskFilterComparator, error) { } } -func getValueForField(field reflect.StructField, rawValue string) (value interface{}, err error) { +func getValueForField(field reflect.StructField, rawValue string, loc *time.Location) (value interface{}, err error) { + + if loc == nil { + loc = config.GetTimeZone() + } + switch field.Type.Kind() { case reflect.Int64: value, err = strconv.ParseInt(rawValue, 10, 64) @@ -245,7 +258,7 @@ func getValueForField(field reflect.StructField, rawValue string) (value interfa var t datemath.Expression t, err = datemath.Parse(rawValue) if err == nil { - value = t.Time(datemath.WithLocation(config.GetTimeZone())) + value = t.Time(datemath.WithLocation(config.GetTimeZone())).In(loc) } else { value, err = parseTimeFromUserInput(rawValue) } @@ -273,7 +286,7 @@ func getValueForField(field reflect.StructField, rawValue string) (value interfa return } -func getNativeValueForTaskField(fieldName string, comparator taskFilterComparator, value string) (reflectField *reflect.StructField, nativeValue interface{}, err error) { +func getNativeValueForTaskField(fieldName string, comparator taskFilterComparator, value string, loc *time.Location) (reflectField *reflect.StructField, nativeValue interface{}, err error) { realFieldName := strings.ReplaceAll(strcase.ToCamel(fieldName), "Id", "ID") @@ -299,7 +312,7 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato vals := strings.Split(value, ",") valueSlice := []interface{}{} for _, val := range vals { - v, err := getValueForField(field, val) + v, err := getValueForField(field, val, loc) if err != nil { return nil, nil, err } @@ -308,6 +321,6 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato return nil, valueSlice, nil } - val, err := getValueForField(field, value) + val, err := getValueForField(field, value, loc) return &field, val, err } diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index c00ce0d00..2f331d482 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -174,6 +174,7 @@ type taskSearchOptions struct { parsedFilters []*taskFilter filterIncludeNulls bool filter string + filterTimezone string projectIDs []int64 } From e09772181766f3340c228801d9e1ad253376fa82 Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 11 Mar 2024 16:39:27 +0100 Subject: [PATCH 15/66] fix(tasks): use correct filter query when filtering --- .../quick-actions/quick-actions.vue | 43 ++-------- frontend/src/composables/useTaskList.ts | 4 +- frontend/src/services/taskCollection.ts | 5 +- frontend/src/stores/kanban.ts | 84 ++++++++----------- frontend/src/stores/tasks.ts | 2 +- frontend/src/views/project/ProjectTable.vue | 4 +- frontend/src/views/tasks/ShowTasks.vue | 22 ++--- 7 files changed, 52 insertions(+), 112 deletions(-) diff --git a/frontend/src/components/quick-actions/quick-actions.vue b/frontend/src/components/quick-actions/quick-actions.vue index 34f8ec947..46e47dd80 100644 --- a/frontend/src/components/quick-actions/quick-actions.vue +++ b/frontend/src/components/quick-actions/quick-actions.vue @@ -350,26 +350,6 @@ const isNewTaskCommand = computed(() => ( const taskSearchTimeout = ref | null>(null) -type Filter = { by: string, value: string | number, comparator: string } - -function filtersToParams(filters: Filter[]) { - const filter_by: Filter['by'][] = [] - const filter_value: Filter['value'][] = [] - const filter_comparator: Filter['comparator'][] = [] - - filters.forEach(({by, value, comparator}) => { - filter_by.push(by) - filter_value.push(value) - filter_comparator.push(comparator) - }) - - return { - filter_by, - filter_value, - filter_comparator, - } -} - function searchTasks() { if ( searchMode.value !== SEARCH_MODE.ALL && @@ -391,40 +371,27 @@ function searchTasks() { const {text, project: projectName, labels} = parsedQuery.value - const filters: Filter[] = [] - - // FIXME: improve types - function addFilter( - by: Filter['by'], - value: Filter['value'], - comparator: Filter['comparator'], - ) { - filters.push({ - by, - value, - comparator, - }) - } - + let filter = '' + if (projectName !== null) { const project = projectStore.findProjectByExactname(projectName) console.log({project}) if (project !== null) { - addFilter('project_id', project.id, 'equals') + filter += ' project = ' + project.id } } if (labels.length > 0) { const labelIds = labelStore.getLabelsByExactTitles(labels).map((l) => l.id) if (labelIds.length > 0) { - addFilter('labels', labelIds.join(), 'in') + filter += 'labels in ' + labelIds.join(', ') } } const params = { s: text, sort_by: 'done', - ...filtersToParams(filters), + filter, } taskSearchTimeout.value = setTimeout(async () => { diff --git a/frontend/src/composables/useTaskList.ts b/frontend/src/composables/useTaskList.ts index 7b05375e8..ef048dc7a 100644 --- a/frontend/src/composables/useTaskList.ts +++ b/frontend/src/composables/useTaskList.ts @@ -2,7 +2,7 @@ import {ref, shallowReactive, watch, computed, type ComputedGetter} from 'vue' import {useRoute} from 'vue-router' import {useRouteQuery} from '@vueuse/router' -import TaskCollectionService, {getDefaultTaskFilterParams} from '@/services/taskCollection' +import TaskCollectionService, {getDefaultTaskFilterParams, type TaskFilterParams} from '@/services/taskCollection' import type {ITask} from '@/modelTypes/ITask' import {error} from '@/message' import type {IProject} from '@/modelTypes/IProject' @@ -58,7 +58,7 @@ export function useTaskList(projectIdGetter: ComputedGetter, sor const projectId = computed(() => projectIdGetter()) - const params = ref({...getDefaultTaskFilterParams()}) + const params = ref({...getDefaultTaskFilterParams()}) const search = ref('') const page = useRouteQuery('page', '1', { transform: Number }) diff --git a/frontend/src/services/taskCollection.ts b/frontend/src/services/taskCollection.ts index 965ad05b3..2cc39773f 100644 --- a/frontend/src/services/taskCollection.ts +++ b/frontend/src/services/taskCollection.ts @@ -4,12 +4,13 @@ import TaskModel from '@/models/task' import type {ITask} from '@/modelTypes/ITask' export interface TaskFilterParams { - sort_by: ('start_date' | 'done' | 'id' | 'position')[], + sort_by: ('start_date' | 'done' | 'id' | 'position' | 'kanban_position')[], order_by: ('asc' | 'desc')[], filter: string, filter_include_nulls: boolean, - filter_timezone: string, + filter_timezone?: string, s: string, + per_page?: number, } export function getDefaultTaskFilterParams(): TaskFilterParams { diff --git a/frontend/src/stores/kanban.ts b/frontend/src/stores/kanban.ts index 6c0e7b044..997cdeac2 100644 --- a/frontend/src/stores/kanban.ts +++ b/frontend/src/stores/kanban.ts @@ -1,5 +1,5 @@ import {computed, readonly, ref} from 'vue' -import {defineStore, acceptHMRUpdate} from 'pinia' +import {acceptHMRUpdate, defineStore} from 'pinia' import {klona} from 'klona/lite' import {findById, findIndexById} from '@/helpers/utils' @@ -20,7 +20,7 @@ const TASKS_PER_BUCKET = 25 function getTaskIndicesById(buckets: IBucket[], taskId: ITask['id']) { let taskIndex - const bucketIndex = buckets.findIndex(({ tasks }) => { + const bucketIndex = buckets.findIndex(({tasks}) => { taskIndex = findIndexById(tasks, taskId) return taskIndex !== -1 }) @@ -28,12 +28,12 @@ function getTaskIndicesById(buckets: IBucket[], taskId: ITask['id']) { return { bucketIndex: bucketIndex !== -1 ? bucketIndex : null, taskIndex: taskIndex !== -1 ? taskIndex : null, - } + } } const addTaskToBucketAndSort = (buckets: IBucket[], task: ITask) => { const bucketIndex = findIndexById(buckets, task.bucketId) - if(typeof buckets[bucketIndex] === 'undefined') { + if (typeof buckets[bucketIndex] === 'undefined') { return } buckets[bucketIndex].tasks.push(task) @@ -46,19 +46,19 @@ const addTaskToBucketAndSort = (buckets: IBucket[], task: ITask) => { */ export const useKanbanStore = defineStore('kanban', () => { const authStore = useAuthStore() - + const buckets = ref([]) const projectId = ref(0) - const bucketLoading = ref<{[id: IBucket['id']]: boolean}>({}) - const taskPagesPerBucket = ref<{[id: IBucket['id']]: number}>({}) - const allTasksLoadedForBucket = ref<{[id: IBucket['id']]: boolean}>({}) + const bucketLoading = ref<{ [id: IBucket['id']]: boolean }>({}) + const taskPagesPerBucket = ref<{ [id: IBucket['id']]: number }>({}) + const allTasksLoadedForBucket = ref<{ [id: IBucket['id']]: boolean }>({}) const isLoading = ref(false) const getBucketById = computed(() => (bucketId: IBucket['id']): IBucket | undefined => findById(buckets.value, bucketId)) const getTaskById = computed(() => { return (id: ITask['id']) => { - const { bucketIndex, taskIndex } = getTaskIndicesById(buckets.value, id) - + const {bucketIndex, taskIndex} = getTaskIndicesById(buckets.value, id) + return { bucketIndex, taskIndex, @@ -98,9 +98,9 @@ export const useKanbanStore = defineStore('kanban', () => { } function setBucketByIndex({ - bucketIndex, - bucket, - } : { + bucketIndex, + bucket, + }: { bucketIndex: number, bucket: IBucket }) { @@ -108,10 +108,10 @@ export const useKanbanStore = defineStore('kanban', () => { } function setTaskInBucketByIndex({ - bucketIndex, - taskIndex, - task, - } : { + bucketIndex, + taskIndex, + task, + }: { bucketIndex: number, taskIndex: number, task: ITask @@ -201,7 +201,7 @@ export const useKanbanStore = defineStore('kanban', () => { return } - const { bucketIndex, taskIndex } = getTaskIndicesById(buckets.value, task.id) + const {bucketIndex, taskIndex} = getTaskIndicesById(buckets.value, task.id) if ( bucketIndex === null || @@ -211,16 +211,16 @@ export const useKanbanStore = defineStore('kanban', () => { ) { return } - + buckets.value[bucketIndex].tasks.splice(taskIndex, 1) buckets.value[bucketIndex].count-- } - function setBucketLoading({bucketId, loading}: {bucketId: IBucket['id'], loading: boolean}) { + function setBucketLoading({bucketId, loading}: { bucketId: IBucket['id'], loading: boolean }) { bucketLoading.value[bucketId] = loading } - function setTasksLoadedForBucketPage({bucketId, page}: {bucketId: IBucket['id'], page: number}) { + function setTasksLoadedForBucketPage({bucketId, page}: { bucketId: IBucket['id'], page: number }) { taskPagesPerBucket.value[bucketId] = page } @@ -228,7 +228,7 @@ export const useKanbanStore = defineStore('kanban', () => { allTasksLoadedForBucket.value[bucketId] = true } - async function loadBucketsForProject({projectId, params}: {projectId: IProject['id'], params}) { + async function loadBucketsForProject({projectId, params}: { projectId: IProject['id'], params }) { const cancel = setModuleLoading(setIsLoading) // Clear everything to prevent having old buckets in the project if loading the buckets from this project takes a few moments @@ -269,29 +269,11 @@ export const useKanbanStore = defineStore('kanban', () => { setBucketLoading({bucketId: bucketId, loading: true}) const params: TaskFilterParams = JSON.parse(JSON.stringify(ps)) - - params.sort_by = 'kanban_position' - params.order_by = 'asc' - - let hasBucketFilter = false - for (const f in params.filter_by) { - if (params.filter_by[f] === 'bucket_id') { - hasBucketFilter = true - if (params.filter_value[f] !== bucketId) { - params.filter_value[f] = bucketId - } - break - } - } - - if (!hasBucketFilter) { - params.filter_by = [...(params.filter_by ?? []), 'bucket_id'] - params.filter_value = [...(params.filter_value ?? []), bucketId] - params.filter_comparator = [...(params.filter_comparator ?? []), 'equals'] - } + params.sort_by = ['kanban_position'] + params.order_by = ['asc'] + params.filter = `${params.filter === '' ? '' : params.filter + ' && '}bucket_id = ${bucketId}` params.filter_timezone = authStore.settings.timezone - params.per_page = TASKS_PER_BUCKET const taskService = new TaskCollectionService() @@ -322,7 +304,7 @@ export const useKanbanStore = defineStore('kanban', () => { } } - async function deleteBucket({bucket, params}: {bucket: IBucket, params}) { + async function deleteBucket({bucket, params}: { bucket: IBucket, params }) { const cancel = setModuleLoading(setIsLoading) const bucketService = new BucketService() @@ -349,13 +331,13 @@ export const useKanbanStore = defineStore('kanban', () => { } setBucketByIndex({bucketIndex, bucket: updatedBucket}) - + const bucketService = new BucketService() try { const returnedBucket = await bucketService.update(updatedBucket) setBucketByIndex({bucketIndex, bucket: returnedBucket}) return returnedBucket - } catch(e) { + } catch (e) { // restore original state setBucketByIndex({bucketIndex, bucket: oldBucket}) @@ -365,7 +347,7 @@ export const useKanbanStore = defineStore('kanban', () => { } } - async function updateBucketTitle({ id, title }: { id: IBucket['id'], title: IBucket['title'] }) { + async function updateBucketTitle({id, title}: { id: IBucket['id'], title: IBucket['title'] }) { const bucket = findById(buckets.value, id) if (bucket?.title === title) { @@ -373,14 +355,14 @@ export const useKanbanStore = defineStore('kanban', () => { return } - await updateBucket({ id, title }) + await updateBucket({id, title}) success({message: i18n.global.t('project.kanban.bucketTitleSavedSuccess')}) } - + return { buckets, isLoading: readonly(isLoading), - + getBucketById, getTaskById, @@ -401,5 +383,5 @@ export const useKanbanStore = defineStore('kanban', () => { // support hot reloading if (import.meta.hot) { - import.meta.hot.accept(acceptHMRUpdate(useKanbanStore, import.meta.hot)) + import.meta.hot.accept(acceptHMRUpdate(useKanbanStore, import.meta.hot)) } \ No newline at end of file diff --git a/frontend/src/stores/tasks.ts b/frontend/src/stores/tasks.ts index 9cf3ab3e5..332a4eba7 100644 --- a/frontend/src/stores/tasks.ts +++ b/frontend/src/stores/tasks.ts @@ -126,7 +126,7 @@ export const useTaskStore = defineStore('task', () => { async function loadTasks(params: TaskFilterParams, projectId: IProject['id'] | null = null) { - if (params.filter_timezone === '') { + if (!params.filter_timezone || params.filter_timezone === '') { params.filter_timezone = authStore.settings.timezone } diff --git a/frontend/src/views/project/ProjectTable.vue b/frontend/src/views/project/ProjectTable.vue index 8ed043f8a..48255f9c1 100644 --- a/frontend/src/views/project/ProjectTable.vue +++ b/frontend/src/views/project/ProjectTable.vue @@ -333,9 +333,7 @@ const { const tasks: Ref = taskList.tasks Object.assign(params.value, { - filter_by: [], - filter_value: [], - filter_comparator: [], + filter: '', }) // FIXME: by doing this we can have multiple sort orders diff --git a/frontend/src/views/tasks/ShowTasks.vue b/frontend/src/views/tasks/ShowTasks.vue index 899edd04c..f947696db 100644 --- a/frontend/src/views/tasks/ShowTasks.vue +++ b/frontend/src/views/tasks/ShowTasks.vue @@ -182,29 +182,21 @@ async function loadPendingTasks(from: string, to: string) { return } - const params = { - sortBy: ['due_date', 'id'], - orderBy: ['asc', 'desc'], - filterTimezone: authStore.settings.timezone, - filterBy: ['done'], - filterValue: ['false'], - filterComparator: ['equals'], - filterConcat: 'and', - filterIncludeNulls: showNulls, + const params: TaskFilterParams = { + sort_by: ['due_date', 'id'], + order_by: ['asc', 'desc'], + filter: 'done = false', + filter_include_nulls: showNulls, } if (!showAll.value) { - params.filterBy.push('due_date') - params.filterValue.push(to) - params.filterComparator.push('less') + params.filter += ` && due_date < '${to}'` // NOTE: Ideally we could also show tasks with a start or end date in the specified range, but the api // is not capable (yet) of combining multiple filters with 'and' and 'or'. if (!showOverdue) { - params.filterBy.push('due_date') - params.filterValue.push(from) - params.filterComparator.push('greater') + params.filter += ` && due_date > '${from}'` } } From 09d51280507849e9d917e00ec74b893c3596366f Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 11 Mar 2024 17:02:04 +0100 Subject: [PATCH 16/66] fix(filters): don't escape valid escaped in queries --- pkg/models/task_collection.go | 10 +++++++++- pkg/models/task_collection_filter.go | 2 +- pkg/models/task_collection_test.go | 12 ++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pkg/models/task_collection.go b/pkg/models/task_collection.go index 4c8160b72..f32658fa4 100644 --- a/pkg/models/task_collection.go +++ b/pkg/models/task_collection.go @@ -36,7 +36,7 @@ type TaskCollection struct { // The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature. Filter string `query:"filter" json:"filter"` // The time zone which should be used for date match (statements like "now" resolve to different actual times) - FilterTimezone string `query:"filter_timezone" json:"filter_timezone"` + FilterTimezone string `query:"filter_timezone" json:"-"` // If set to true, the result will also include null values FilterIncludeNulls bool `query:"filter_include_nulls" json:"filter_include_nulls"` @@ -158,6 +158,14 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa sf.Filters.OrderBy = orderby sf.Filters.OrderByArr = nil + if sf.Filters.FilterTimezone == "" { + u, err := user.GetUserByID(s, a.GetID()) + if err != nil { + return nil, 0, 0, err + } + sf.Filters.FilterTimezone = u.Timezone + } + return sf.getTaskCollection().ReadAll(s, a, search, page, perPage) } diff --git a/pkg/models/task_collection_filter.go b/pkg/models/task_collection_filter.go index d0885ff4d..9ac4ad847 100644 --- a/pkg/models/task_collection_filter.go +++ b/pkg/models/task_collection_filter.go @@ -155,7 +155,7 @@ func getTaskFiltersFromFilterString(filter string, filterTimezone string) (filte filter = strings.ReplaceAll(filter, " in ", " ?= ") // Replaces all occurrences with in with a string so that it passes the filter - pattern := `\?=\s+([^&|]+)` + pattern := `\?=\s+([^&|']+)` re := regexp.MustCompile(pattern) filter = re.ReplaceAllStringFunc(filter, func(match string) string { diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index a8c2e852a..0745b586a 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -1044,6 +1044,18 @@ func TestTaskCollection_ReadAll(t *testing.T) { }, wantErr: false, }, + { + name: "filter in keyword without quotes", + fields: fields{ + Filter: "id in 1,2,34", // user does not have permission to access task 34 + }, + args: defaultArgs, + want: []*Task{ + task1, + task2, + }, + wantErr: false, + }, { name: "filter in", fields: fields{ From 0910d5d2f236d478cbf00e6fa6fc8ebd8ff98bd0 Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 11 Mar 2024 17:20:05 +0100 Subject: [PATCH 17/66] chore(auth): refactor removing empty openid teams to cron job --- pkg/initialize/init.go | 1 + pkg/modules/auth/openid/cron.go | 68 ++++++++++++++++++++++++++ pkg/modules/auth/openid/openid.go | 22 +-------- pkg/modules/auth/openid/openid_test.go | 40 --------------- 4 files changed, 70 insertions(+), 61 deletions(-) create mode 100644 pkg/modules/auth/openid/cron.go diff --git a/pkg/initialize/init.go b/pkg/initialize/init.go index 7d25bb882..0e72e659b 100644 --- a/pkg/initialize/init.go +++ b/pkg/initialize/init.go @@ -95,6 +95,7 @@ func FullInit() { models.RegisterUserDeletionCron() models.RegisterOldExportCleanupCron() openid.CleanupSavedOpenIDProviders() + openid.RegisterEmptyOpenIDTeamCleanupCron() // Start processing events go func() { diff --git a/pkg/modules/auth/openid/cron.go b/pkg/modules/auth/openid/cron.go new file mode 100644 index 000000000..96ae5c175 --- /dev/null +++ b/pkg/modules/auth/openid/cron.go @@ -0,0 +1,68 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package openid + +import ( + "code.vikunja.io/api/pkg/log" + "code.vikunja.io/api/pkg/models" + "xorm.io/builder" + "xorm.io/xorm" + + "code.vikunja.io/api/pkg/cron" + "code.vikunja.io/api/pkg/db" +) + +func RemoveEmptySSOTeams(s *xorm.Session) (err error) { + teams := []*models.Team{} + err = s. + Where( + builder.NotIn("id", builder.Expr("select team_members.team_id from team_members")), + builder.Or(builder.Neq{"oidc_id": ""}, builder.NotNull{"oidc_id"}), + ). + Find(&teams) + if err != nil { + return err + } + + teamIDs := make([]int64, 0, len(teams)) + for _, team := range teams { + teamIDs = append(teamIDs, team.ID) + } + + log.Debugf("Deleting empty teams: %v", teamIDs) + + _, err = s.In("id", teamIDs).Delete(&models.Team{}) + return err +} + +func RegisterEmptyOpenIDTeamCleanupCron() { + const logPrefix = "[Empty openid Team Cleanup Cron] " + + err := cron.Schedule("* * * * *", func() { + s := db.NewSession() + defer s.Close() + + err := RemoveEmptySSOTeams(s) + if err != nil { + log.Errorf(logPrefix+"Error removing empty openid team: %s", err) + return + } + }) + if err != nil { + log.Fatalf("Could not empty openid teams cleanup cron: %s", err) + } +} diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index ad9655315..c2ee5dcf1 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -222,13 +222,7 @@ func HandleCallback(c echo.Context) error { teamIDsToLeave := utils.NotIn(oldOidcTeams, oidcTeams) err = RemoveUserFromTeamsByIDs(s, u, teamIDsToLeave) if err != nil { - log.Errorf("Found error while leaving teams %v", err) - } - errs := RemoveEmptySSOTeams(s, teamIDsToLeave) - if len(errs) > 0 { - for _, err := range errs { - log.Errorf("Found error while removing empty teams %v", err) - } + log.Errorf("Error while leaving teams %v", err) } } err = s.Commit() @@ -266,20 +260,6 @@ func AssignOrCreateUserToTeams(s *xorm.Session, u *user.User, teamData []*models return oidcTeams, err } -func RemoveEmptySSOTeams(s *xorm.Session, teamIDs []int64) (errs []error) { - for _, teamID := range teamIDs { - count, err := s.Where("team_id = ?", teamID).Count(&models.TeamMember{}) - if count == 0 && err == nil { - log.Debugf("SSO team with id %v has no members. It will be deleted", teamID) - _, _err := s.Where("id = ?", teamID).Delete(&models.Team{}) - if _err != nil { - errs = append(errs, _err) - } - } - } - return errs -} - func RemoveUserFromTeamsByIDs(s *xorm.Session, u *user.User, teamIDs []int64) (err error) { if len(teamIDs) < 1 { diff --git a/pkg/modules/auth/openid/openid_test.go b/pkg/modules/auth/openid/openid_test.go index b1ebcdaa7..e50f26d81 100644 --- a/pkg/modules/auth/openid/openid_test.go +++ b/pkg/modules/auth/openid/openid_test.go @@ -220,14 +220,6 @@ func TestGetOrCreateUser(t *testing.T) { require.NoError(t, err) err = RemoveUserFromTeamsByIDs(s, u, teamIDsToLeave) require.NoError(t, err) - errs = RemoveEmptySSOTeams(s, teamIDsToLeave) - for _, err = range errs { - require.NoError(t, err) - } - errs = RemoveEmptySSOTeams(s, teamIDsToLeave) - for _, err = range errs { - require.NoError(t, err) - } err = s.Commit() require.NoError(t, err) @@ -235,38 +227,6 @@ func TestGetOrCreateUser(t *testing.T) { "team_id": oidcTeams, "user_id": u.ID, }) - }) - t.Run("existing user, remove from existing team and delete team", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - defer s.Close() - - cl := &claims{ - Email: "other-email-address@some.service.com", - VikunjaGroups: []map[string]interface{}{}, - } - - u := &user.User{ID: 10} - teamData, errs := getTeamDataFromToken(cl.VikunjaGroups, nil) - if len(errs) > 0 { - for _, err := range errs { - require.NoError(t, err) - } - } - oldOidcTeams, err := models.FindAllOidcTeamIDsForUser(s, u.ID) - require.NoError(t, err) - oidcTeams, err := AssignOrCreateUserToTeams(s, u, teamData, "https://some.issuer") - require.NoError(t, err) - teamIDsToLeave := utils.NotIn(oldOidcTeams, oidcTeams) - require.NoError(t, err) - err = RemoveUserFromTeamsByIDs(s, u, teamIDsToLeave) - require.NoError(t, err) - errs = RemoveEmptySSOTeams(s, teamIDsToLeave) - for _, err := range errs { - require.NoError(t, err) - } - err = s.Commit() - require.NoError(t, err) db.AssertMissing(t, "teams", map[string]interface{}{ "id": oidcTeams, }) From 49ab90fc19f9da7d1308c923d6dd99b8a6a355ef Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 11 Mar 2024 17:24:40 +0100 Subject: [PATCH 18/66] fix: lint --- frontend/src/stores/kanban.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/frontend/src/stores/kanban.ts b/frontend/src/stores/kanban.ts index 997cdeac2..67cbfe988 100644 --- a/frontend/src/stores/kanban.ts +++ b/frontend/src/stores/kanban.ts @@ -97,13 +97,10 @@ export const useKanbanStore = defineStore('kanban', () => { buckets.value[bucketIndex] = newBucket } - function setBucketByIndex({ - bucketIndex, - bucket, - }: { + function setBucketByIndex( bucketIndex: number, - bucket: IBucket - }) { + bucket: IBucket, + ) { buckets.value[bucketIndex] = bucket } @@ -269,7 +266,7 @@ export const useKanbanStore = defineStore('kanban', () => { setBucketLoading({bucketId: bucketId, loading: true}) const params: TaskFilterParams = JSON.parse(JSON.stringify(ps)) - + params.sort_by = ['kanban_position'] params.order_by = ['asc'] params.filter = `${params.filter === '' ? '' : params.filter + ' && '}bucket_id = ${bucketId}` @@ -330,16 +327,16 @@ export const useKanbanStore = defineStore('kanban', () => { ...updatedBucketData, } - setBucketByIndex({bucketIndex, bucket: updatedBucket}) + setBucketByIndex(bucketIndex, updatedBucket) const bucketService = new BucketService() try { const returnedBucket = await bucketService.update(updatedBucket) - setBucketByIndex({bucketIndex, bucket: returnedBucket}) + setBucketByIndex(bucketIndex, returnedBucket) return returnedBucket } catch (e) { // restore original state - setBucketByIndex({bucketIndex, bucket: oldBucket}) + setBucketByIndex(bucketIndex, oldBucket) throw e } finally { From 659de54db1ffcdc0a6722dfdf65a245e24846ed9 Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 11 Mar 2024 17:29:28 +0100 Subject: [PATCH 19/66] feat(kanban): do not remove focus from the input after creating a new bucket --- frontend/src/views/project/ProjectKanban.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/views/project/ProjectKanban.vue b/frontend/src/views/project/ProjectKanban.vue index ab46a5cb1..9793235bb 100644 --- a/frontend/src/views/project/ProjectKanban.vue +++ b/frontend/src/views/project/ProjectKanban.vue @@ -554,7 +554,6 @@ async function createNewBucket() { projectId: project.value.id, })) newBucketTitle.value = '' - showNewBucketInput.value = false } function deleteBucketModal(bucketId: IBucket['id']) { From 3f380e0d61e78b829310f9cfd8c6a176a21976fd Mon Sep 17 00:00:00 2001 From: "Frederick [Bot]" Date: Mon, 11 Mar 2024 16:41:16 +0000 Subject: [PATCH 20/66] [skip ci] Updated swagger docs --- pkg/swagger/docs.go | 13 +++++++++++++ pkg/swagger/swagger.json | 13 +++++++++++++ pkg/swagger/swagger.yaml | 12 ++++++++++++ 3 files changed, 38 insertions(+) diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index b67243664..0fa109f4d 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -1941,6 +1941,12 @@ const docTemplate = `{ "name": "filter", "in": "query" }, + { + "type": "string", + "description": "The time zone which should be used for date match (statements like ", + "name": "filter_timezone", + "in": "query" + }, { "type": "string", "description": "If set to true the result will include filtered fields whose value is set to ` + "`" + `null` + "`" + `. Available values are ` + "`" + `true` + "`" + ` or ` + "`" + `false` + "`" + `. Defaults to ` + "`" + `false` + "`" + `.", @@ -2155,6 +2161,12 @@ const docTemplate = `{ "name": "filter", "in": "query" }, + { + "type": "string", + "description": "The time zone which should be used for date match (statements like ", + "name": "filter_timezone", + "in": "query" + }, { "type": "string", "description": "If set to true the result will include filtered fields whose value is set to ` + "`" + `null` + "`" + `. Available values are ` + "`" + `true` + "`" + ` or ` + "`" + `false` + "`" + `. Defaults to ` + "`" + `false` + "`" + `.", @@ -8097,6 +8109,7 @@ const docTemplate = `{ "type": "object", "properties": { "filter": { + "description": "The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature.", "type": "string" }, "filter_include_nulls": { diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 836a1baa4..0bc6e5b2e 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -1933,6 +1933,12 @@ "name": "filter", "in": "query" }, + { + "type": "string", + "description": "The time zone which should be used for date match (statements like ", + "name": "filter_timezone", + "in": "query" + }, { "type": "string", "description": "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`.", @@ -2147,6 +2153,12 @@ "name": "filter", "in": "query" }, + { + "type": "string", + "description": "The time zone which should be used for date match (statements like ", + "name": "filter_timezone", + "in": "query" + }, { "type": "string", "description": "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`.", @@ -8089,6 +8101,7 @@ "type": "object", "properties": { "filter": { + "description": "The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature.", "type": "string" }, "filter_include_nulls": { diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 1365bd24d..5f6722b42 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -784,6 +784,8 @@ definitions: models.TaskCollection: properties: filter: + description: The filter query to match tasks by. Check out https://vikunja.io/docs/filters + for a full explanation of the feature. type: string filter_include_nulls: description: If set to true, the result will also include null values @@ -2724,6 +2726,11 @@ paths: in: query name: filter type: string + - description: 'The time zone which should be used for date match (statements + like ' + in: query + name: filter_timezone + type: string - description: If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`. @@ -2874,6 +2881,11 @@ paths: in: query name: filter type: string + - description: 'The time zone which should be used for date match (statements + like ' + in: query + name: filter_timezone + type: string - description: If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`. From 85fb8e3443d19e36edd0033bef52ea00f13b720e Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 11 Mar 2024 23:28:35 +0100 Subject: [PATCH 21/66] fix(filters): invalid filter range when converting dates to strings Resolves https://community.vikunja.io/t/my-vikunja-instance-creates-tasks-with-due-date-time-of-9am-for-tasks-with-the-word-today-word-in-it/2105/10 --- frontend/src/services/taskCollection.ts | 2 +- frontend/src/views/tasks/ShowTasks.vue | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/services/taskCollection.ts b/frontend/src/services/taskCollection.ts index 2cc39773f..f83dac2e2 100644 --- a/frontend/src/services/taskCollection.ts +++ b/frontend/src/services/taskCollection.ts @@ -4,7 +4,7 @@ import TaskModel from '@/models/task' import type {ITask} from '@/modelTypes/ITask' export interface TaskFilterParams { - sort_by: ('start_date' | 'done' | 'id' | 'position' | 'kanban_position')[], + sort_by: ('start_date' | 'end_date' | 'due_date' | 'done' | 'id' | 'position' | 'kanban_position')[], order_by: ('asc' | 'desc')[], filter: string, filter_include_nulls: boolean, diff --git a/frontend/src/views/tasks/ShowTasks.vue b/frontend/src/views/tasks/ShowTasks.vue index f947696db..0a5318051 100644 --- a/frontend/src/views/tasks/ShowTasks.vue +++ b/frontend/src/views/tasks/ShowTasks.vue @@ -173,7 +173,7 @@ function setShowNulls(show: boolean) { }) } -async function loadPendingTasks(from: string, to: string) { +async function loadPendingTasks(from: Date|string, to: Date|string) { // FIXME: HACK! This should never happen. // Since this route is authentication only, users would get an error message if they access the page unauthenticated. // Since this component is mounted as the home page before unauthenticated users get redirected @@ -187,16 +187,18 @@ async function loadPendingTasks(from: string, to: string) { order_by: ['asc', 'desc'], filter: 'done = false', filter_include_nulls: showNulls, + s: '', } if (!showAll.value) { - params.filter += ` && due_date < '${to}'` + + params.filter += ` && due_date < '${to instanceof Date ? to.toISOString() : to}'` // NOTE: Ideally we could also show tasks with a start or end date in the specified range, but the api // is not capable (yet) of combining multiple filters with 'and' and 'or'. if (!showOverdue) { - params.filter += ` && due_date > '${from}'` + params.filter += ` && due_date > '${from instanceof Date ? from.toISOString() : from}'` } } From da53c8e7ef0a2a88a6f1bdad82a12394582d8e03 Mon Sep 17 00:00:00 2001 From: renovate Date: Tue, 12 Mar 2024 06:07:20 +0000 Subject: [PATCH 22/66] chore(deps): update dev-dependencies --- frontend/package.json | 14 +- frontend/pnpm-lock.yaml | 332 ++++++++++++++++++++-------------------- 2 files changed, 173 insertions(+), 173 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 9f247aecc..f000a5356 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -141,11 +141,11 @@ "@types/is-touch-device": "1.0.2", "@types/lodash.debounce": "4.0.9", "@types/marked": "5.0.2", - "@types/node": "20.11.25", + "@types/node": "20.11.26", "@types/postcss-preset-env": "7.7.0", "@types/sortablejs": "1.15.8", - "@typescript-eslint/eslint-plugin": "7.1.1", - "@typescript-eslint/parser": "7.1.1", + "@typescript-eslint/eslint-plugin": "7.2.0", + "@typescript-eslint/parser": "7.2.0", "@vitejs/plugin-legacy": "5.3.2", "@vitejs/plugin-vue": "5.0.4", "@vue/eslint-config-typescript": "13.0.0", @@ -159,20 +159,20 @@ "cypress": "13.6.6", "esbuild": "0.20.1", "eslint": "8.57.0", - "eslint-plugin-vue": "9.22.0", - "happy-dom": "13.7.3", + "eslint-plugin-vue": "9.23.0", + "happy-dom": "13.7.8", "histoire": "0.17.9", "postcss": "8.4.35", "postcss-easing-gradients": "3.0.1", "postcss-easings": "4.0.0", "postcss-focus-within": "8.0.1", "postcss-preset-env": "9.5.0", - "rollup": "4.12.1", + "rollup": "4.13.0", "rollup-plugin-visualizer": "5.12.0", "sass": "1.71.1", "start-server-and-test": "2.0.3", "typescript": "5.4.2", - "vite": "5.1.5", + "vite": "5.1.6", "vite-plugin-inject-preload": "1.3.3", "vite-plugin-pwa": "0.19.2", "vite-plugin-sentry": "1.4.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 31ddb804f..8806e06dc 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -30,7 +30,7 @@ dependencies: version: 2.2.0(dayjs@1.11.10)(vue@3.4.21) '@intlify/unplugin-vue-i18n': specifier: 3.0.1 - version: 3.0.1(rollup@4.12.1)(vue-i18n@9.10.1) + version: 3.0.1(rollup@4.13.0)(vue-i18n@9.10.1) '@kyvg/vue3-notification': specifier: 3.2.0 version: 3.2.0(vue@3.4.21) @@ -254,7 +254,7 @@ devDependencies: version: 0.17.8(histoire@0.17.9) '@histoire/plugin-vue': specifier: 0.17.12 - version: 0.17.12(histoire@0.17.9)(vite@5.1.5)(vue@3.4.21) + version: 0.17.12(histoire@0.17.9)(vite@5.1.6)(vue@3.4.21) '@rushstack/eslint-patch': specifier: 1.7.2 version: 1.7.2 @@ -277,8 +277,8 @@ devDependencies: specifier: 5.0.2 version: 5.0.2 '@types/node': - specifier: 20.11.25 - version: 20.11.25 + specifier: 20.11.26 + version: 20.11.26 '@types/postcss-preset-env': specifier: 7.7.0 version: 7.7.0 @@ -286,20 +286,20 @@ devDependencies: specifier: 1.15.8 version: 1.15.8 '@typescript-eslint/eslint-plugin': - specifier: 7.1.1 - version: 7.1.1(@typescript-eslint/parser@7.1.1)(eslint@8.57.0)(typescript@5.4.2) + specifier: 7.2.0 + version: 7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/parser': - specifier: 7.1.1 - version: 7.1.1(eslint@8.57.0)(typescript@5.4.2) + specifier: 7.2.0 + version: 7.2.0(eslint@8.57.0)(typescript@5.4.2) '@vitejs/plugin-legacy': specifier: 5.3.2 - version: 5.3.2(terser@5.24.0)(vite@5.1.5) + version: 5.3.2(terser@5.24.0)(vite@5.1.6) '@vitejs/plugin-vue': specifier: 5.0.4 - version: 5.0.4(vite@5.1.5)(vue@3.4.21) + version: 5.0.4(vite@5.1.6)(vue@3.4.21) '@vue/eslint-config-typescript': specifier: 13.0.0 - version: 13.0.0(eslint-plugin-vue@9.22.0)(eslint@8.57.0)(typescript@5.4.2) + version: 13.0.0(eslint-plugin-vue@9.23.0)(eslint@8.57.0)(typescript@5.4.2) '@vue/test-utils': specifier: 2.4.4 version: 2.4.4(vue@3.4.21) @@ -331,14 +331,14 @@ devDependencies: specifier: 8.57.0 version: 8.57.0 eslint-plugin-vue: - specifier: 9.22.0 - version: 9.22.0(eslint@8.57.0) + specifier: 9.23.0 + version: 9.23.0(eslint@8.57.0) happy-dom: - specifier: 13.7.3 - version: 13.7.3 + specifier: 13.7.8 + version: 13.7.8 histoire: specifier: 0.17.9 - version: 0.17.9(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0)(vite@5.1.5) + version: 0.17.9(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0)(vite@5.1.6) postcss: specifier: 8.4.35 version: 8.4.35 @@ -355,11 +355,11 @@ devDependencies: specifier: 9.5.0 version: 9.5.0(postcss@8.4.35) rollup: - specifier: 4.12.1 - version: 4.12.1 + specifier: 4.13.0 + version: 4.13.0 rollup-plugin-visualizer: specifier: 5.12.0 - version: 5.12.0(rollup@4.12.1) + version: 5.12.0(rollup@4.13.0) sass: specifier: 1.71.1 version: 1.71.1 @@ -370,23 +370,23 @@ devDependencies: specifier: 5.4.2 version: 5.4.2 vite: - specifier: 5.1.5 - version: 5.1.5(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0) + specifier: 5.1.6 + version: 5.1.6(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0) vite-plugin-inject-preload: specifier: 1.3.3 - version: 1.3.3(vite@5.1.5) + version: 1.3.3(vite@5.1.6) vite-plugin-pwa: specifier: 0.19.2 - version: 0.19.2(vite@5.1.5)(workbox-build@7.0.0)(workbox-window@7.0.0) + version: 0.19.2(vite@5.1.6)(workbox-build@7.0.0)(workbox-window@7.0.0) vite-plugin-sentry: specifier: 1.4.0 - version: 1.4.0(vite@5.1.5) + version: 1.4.0(vite@5.1.6) vite-svg-loader: specifier: 5.1.0 version: 5.1.0(vue@3.4.21) vitest: specifier: 1.3.1 - version: 1.3.1(@types/node@20.11.25)(happy-dom@13.7.3)(sass@1.71.1)(terser@5.24.0) + version: 1.3.1(@types/node@20.11.26)(happy-dom@13.7.8)(sass@1.71.1)(terser@5.24.0) vue-tsc: specifier: 2.0.6 version: 2.0.6(typescript@5.4.2) @@ -2701,11 +2701,11 @@ packages: '@hapi/hoek': 9.2.1 dev: true - /@histoire/app@0.17.9(vite@5.1.5): + /@histoire/app@0.17.9(vite@5.1.6): resolution: {integrity: sha512-JoSGbsoo1/JY5TtTiMBUSPllIEJLvC6jHIGruvwPG/cJ3niqa3EyEMOsOWtcu+xjtx1uETgL9Yj5RJMJjC+OBA==} dependencies: - '@histoire/controls': 0.17.9(vite@5.1.5) - '@histoire/shared': 0.17.9(vite@5.1.5) + '@histoire/controls': 0.17.9(vite@5.1.6) + '@histoire/shared': 0.17.9(vite@5.1.6) '@histoire/vendors': 0.17.8 '@types/flexsearch': 0.7.6 flexsearch: 0.7.21 @@ -2714,7 +2714,7 @@ packages: - vite dev: true - /@histoire/controls@0.17.9(vite@5.1.5): + /@histoire/controls@0.17.9(vite@5.1.6): resolution: {integrity: sha512-1f1cE1NZ2emzGMRnGfAb/gCKDtBT3bUZzj3aAcDmhm3MA2Vy5tGYSb9j+KuTTj7+exhOrKefmedr9a0q1/5g2w==} dependencies: '@codemirror/commands': 6.3.2 @@ -2724,7 +2724,7 @@ packages: '@codemirror/state': 6.3.2 '@codemirror/theme-one-dark': 6.1.2 '@codemirror/view': 6.22.1 - '@histoire/shared': 0.17.9(vite@5.1.5) + '@histoire/shared': 0.17.9(vite@5.1.6) '@histoire/vendors': 0.17.8 transitivePeerDependencies: - vite @@ -2738,7 +2738,7 @@ packages: capture-website: 2.4.1 defu: 6.1.3 fs-extra: 10.1.0 - histoire: 0.17.9(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0)(vite@5.1.5) + histoire: 0.17.9(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0)(vite@5.1.6) pathe: 1.1.1 transitivePeerDependencies: - bufferutil @@ -2747,18 +2747,18 @@ packages: - utf-8-validate dev: true - /@histoire/plugin-vue@0.17.12(histoire@0.17.9)(vite@5.1.5)(vue@3.4.21): + /@histoire/plugin-vue@0.17.12(histoire@0.17.9)(vite@5.1.6)(vue@3.4.21): resolution: {integrity: sha512-mpx2uwHq/qemnX+ARQtDR3M9kIt1y4kBCmzBkOquhJTp61mtHMu4hZKSzzQpQWA2QxEyuuwpaNiU7Mlms13EaQ==} peerDependencies: histoire: ^0.17.9 vue: ^3.2.47 dependencies: - '@histoire/controls': 0.17.9(vite@5.1.5) - '@histoire/shared': 0.17.10(vite@5.1.5) + '@histoire/controls': 0.17.9(vite@5.1.6) + '@histoire/shared': 0.17.10(vite@5.1.6) '@histoire/vendors': 0.17.8 change-case: 4.1.2 globby: 13.2.2 - histoire: 0.17.9(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0)(vite@5.1.5) + histoire: 0.17.9(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0)(vite@5.1.6) launch-editor: 2.6.1 pathe: 1.1.1 vue: 3.4.21(typescript@5.4.2) @@ -2766,7 +2766,7 @@ packages: - vite dev: true - /@histoire/shared@0.17.10(vite@5.1.5): + /@histoire/shared@0.17.10(vite@5.1.6): resolution: {integrity: sha512-8hzk/WKASrYfaJ+UtR6Mv7aZlP8IZvQ5POoHAi+JvHMJTtzCXZeuL0qdQAXg0zdk3vWIH20oSl6N8hZE1AP7yA==} peerDependencies: vite: ^2.9.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 @@ -2777,10 +2777,10 @@ packages: chokidar: 3.5.3 pathe: 1.1.1 picocolors: 1.0.0 - vite: 5.1.5(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0) + vite: 5.1.6(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0) dev: true - /@histoire/shared@0.17.9(vite@5.1.5): + /@histoire/shared@0.17.9(vite@5.1.6): resolution: {integrity: sha512-E/l4EzYc69/bOImUnvfi7h4/DHGl1rc96lkuMYulL5hjRjuNhSy5AlN5bG0nkVOG4RVIAnLGevMaMi207wtvLw==} peerDependencies: vite: ^2.9.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 @@ -2791,7 +2791,7 @@ packages: chokidar: 3.5.3 pathe: 1.1.1 picocolors: 1.0.0 - vite: 5.1.5(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0) + vite: 5.1.6(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0) dev: true /@histoire/vendors@0.17.8: @@ -2877,7 +2877,7 @@ packages: engines: {node: '>= 16'} dev: false - /@intlify/unplugin-vue-i18n@3.0.1(rollup@4.12.1)(vue-i18n@9.10.1): + /@intlify/unplugin-vue-i18n@3.0.1(rollup@4.13.0)(vue-i18n@9.10.1): resolution: {integrity: sha512-q1zJhA/WpoLBzAAuKA5/AEp0e+bMOM10ll/HxT4g1VAw/9JhC4TTobP9KobKH90JMZ4U2daLFlYQfKNd29lpqw==} engines: {node: '>= 14.16'} peerDependencies: @@ -2894,7 +2894,7 @@ packages: dependencies: '@intlify/bundle-utils': 7.4.0(vue-i18n@9.10.1) '@intlify/shared': 9.10.1 - '@rollup/pluginutils': 5.1.0(rollup@4.12.1) + '@rollup/pluginutils': 5.1.0(rollup@4.13.0) '@vue/compiler-sfc': 3.4.21 debug: 4.3.4(supports-color@8.1.1) fast-glob: 3.3.2 @@ -3133,7 +3133,7 @@ packages: rollup: 2.79.1 dev: true - /@rollup/pluginutils@5.1.0(rollup@4.12.1): + /@rollup/pluginutils@5.1.0(rollup@4.13.0): resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} engines: {node: '>=14.0.0'} peerDependencies: @@ -3145,95 +3145,95 @@ packages: '@types/estree': 1.0.5 estree-walker: 2.0.2 picomatch: 2.3.1 - rollup: 4.12.1 + rollup: 4.13.0 dev: false - /@rollup/rollup-android-arm-eabi@4.12.1: - resolution: {integrity: sha512-iU2Sya8hNn1LhsYyf0N+L4Gf9Qc+9eBTJJJsaOGUp+7x4n2M9dxTt8UvhJl3oeftSjblSlpCfvjA/IfP3g5VjQ==} + /@rollup/rollup-android-arm-eabi@4.13.0: + resolution: {integrity: sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==} cpu: [arm] os: [android] requiresBuild: true optional: true - /@rollup/rollup-android-arm64@4.12.1: - resolution: {integrity: sha512-wlzcWiH2Ir7rdMELxFE5vuM7D6TsOcJ2Yw0c3vaBR3VOsJFVTx9xvwnAvhgU5Ii8Gd6+I11qNHwndDscIm0HXg==} + /@rollup/rollup-android-arm64@4.13.0: + resolution: {integrity: sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==} cpu: [arm64] os: [android] requiresBuild: true optional: true - /@rollup/rollup-darwin-arm64@4.12.1: - resolution: {integrity: sha512-YRXa1+aZIFN5BaImK+84B3uNK8C6+ynKLPgvn29X9s0LTVCByp54TB7tdSMHDR7GTV39bz1lOmlLDuedgTwwHg==} + /@rollup/rollup-darwin-arm64@4.13.0: + resolution: {integrity: sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==} cpu: [arm64] os: [darwin] requiresBuild: true optional: true - /@rollup/rollup-darwin-x64@4.12.1: - resolution: {integrity: sha512-opjWJ4MevxeA8FhlngQWPBOvVWYNPFkq6/25rGgG+KOy0r8clYwL1CFd+PGwRqqMFVQ4/Qd3sQu5t7ucP7C/Uw==} + /@rollup/rollup-darwin-x64@4.13.0: + resolution: {integrity: sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==} cpu: [x64] os: [darwin] requiresBuild: true optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.12.1: - resolution: {integrity: sha512-uBkwaI+gBUlIe+EfbNnY5xNyXuhZbDSx2nzzW8tRMjUmpScd6lCQYKY2V9BATHtv5Ef2OBq6SChEP8h+/cxifQ==} + /@rollup/rollup-linux-arm-gnueabihf@4.13.0: + resolution: {integrity: sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==} cpu: [arm] os: [linux] requiresBuild: true optional: true - /@rollup/rollup-linux-arm64-gnu@4.12.1: - resolution: {integrity: sha512-0bK9aG1kIg0Su7OcFTlexkVeNZ5IzEsnz1ept87a0TUgZ6HplSgkJAnFpEVRW7GRcikT4GlPV0pbtVedOaXHQQ==} + /@rollup/rollup-linux-arm64-gnu@4.13.0: + resolution: {integrity: sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==} cpu: [arm64] os: [linux] requiresBuild: true optional: true - /@rollup/rollup-linux-arm64-musl@4.12.1: - resolution: {integrity: sha512-qB6AFRXuP8bdkBI4D7UPUbE7OQf7u5OL+R94JE42Z2Qjmyj74FtDdLGeriRyBDhm4rQSvqAGCGC01b8Fu2LthQ==} + /@rollup/rollup-linux-arm64-musl@4.13.0: + resolution: {integrity: sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==} cpu: [arm64] os: [linux] requiresBuild: true optional: true - /@rollup/rollup-linux-riscv64-gnu@4.12.1: - resolution: {integrity: sha512-sHig3LaGlpNgDj5o8uPEoGs98RII8HpNIqFtAI8/pYABO8i0nb1QzT0JDoXF/pxzqO+FkxvwkHZo9k0NJYDedg==} + /@rollup/rollup-linux-riscv64-gnu@4.13.0: + resolution: {integrity: sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==} cpu: [riscv64] os: [linux] requiresBuild: true optional: true - /@rollup/rollup-linux-x64-gnu@4.12.1: - resolution: {integrity: sha512-nD3YcUv6jBJbBNFvSbp0IV66+ba/1teuBcu+fBBPZ33sidxitc6ErhON3JNavaH8HlswhWMC3s5rgZpM4MtPqQ==} + /@rollup/rollup-linux-x64-gnu@4.13.0: + resolution: {integrity: sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==} cpu: [x64] os: [linux] requiresBuild: true optional: true - /@rollup/rollup-linux-x64-musl@4.12.1: - resolution: {integrity: sha512-7/XVZqgBby2qp/cO0TQ8uJK+9xnSdJ9ct6gSDdEr4MfABrjTyrW6Bau7HQ73a2a5tPB7hno49A0y1jhWGDN9OQ==} + /@rollup/rollup-linux-x64-musl@4.13.0: + resolution: {integrity: sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==} cpu: [x64] os: [linux] requiresBuild: true optional: true - /@rollup/rollup-win32-arm64-msvc@4.12.1: - resolution: {integrity: sha512-CYc64bnICG42UPL7TrhIwsJW4QcKkIt9gGlj21gq3VV0LL6XNb1yAdHVp1pIi9gkts9gGcT3OfUYHjGP7ETAiw==} + /@rollup/rollup-win32-arm64-msvc@4.13.0: + resolution: {integrity: sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==} cpu: [arm64] os: [win32] requiresBuild: true optional: true - /@rollup/rollup-win32-ia32-msvc@4.12.1: - resolution: {integrity: sha512-LN+vnlZ9g0qlHGlS920GR4zFCqAwbv2lULrR29yGaWP9u7wF5L7GqWu9Ah6/kFZPXPUkpdZwd//TNR+9XC9hvA==} + /@rollup/rollup-win32-ia32-msvc@4.13.0: + resolution: {integrity: sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==} cpu: [ia32] os: [win32] requiresBuild: true optional: true - /@rollup/rollup-win32-x64-msvc@4.12.1: - resolution: {integrity: sha512-n+vkrSyphvmU0qkQ6QBNXCGr2mKjhP08mPRM/Xp5Ck2FV4NrHU+y6axzDeixUrCBHVUS51TZhjqrKBBsHLKb2Q==} + /@rollup/rollup-win32-x64-msvc@4.13.0: + resolution: {integrity: sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==} cpu: [x64] os: [win32] requiresBuild: true @@ -3794,7 +3794,7 @@ packages: /@types/fs-extra@9.0.13: resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} dependencies: - '@types/node': 20.11.25 + '@types/node': 20.11.26 dev: true /@types/har-format@1.2.10: @@ -3818,7 +3818,7 @@ packages: /@types/keyv@3.1.3: resolution: {integrity: sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg==} dependencies: - '@types/node': 20.11.25 + '@types/node': 20.11.26 dev: true /@types/linkify-it@3.0.2: @@ -3859,8 +3859,8 @@ packages: resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} dev: true - /@types/node@20.11.25: - resolution: {integrity: sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==} + /@types/node@20.11.26: + resolution: {integrity: sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==} dependencies: undici-types: 5.26.5 dev: true @@ -3887,13 +3887,13 @@ packages: /@types/resolve@1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: - '@types/node': 20.11.25 + '@types/node': 20.11.26 dev: true /@types/responselike@1.0.0: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} dependencies: - '@types/node': 20.11.25 + '@types/node': 20.11.26 dev: true /@types/semver@7.5.0: @@ -3942,12 +3942,12 @@ packages: resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} requiresBuild: true dependencies: - '@types/node': 20.11.25 + '@types/node': 20.11.26 dev: true optional: true - /@typescript-eslint/eslint-plugin@7.1.1(@typescript-eslint/parser@7.1.1)(eslint@8.57.0)(typescript@5.4.2): - resolution: {integrity: sha512-zioDz623d0RHNhvx0eesUmGfIjzrk18nSBC8xewepKXbBvN/7c1qImV7Hg8TI1URTxKax7/zxfxj3Uph8Chcuw==} + /@typescript-eslint/eslint-plugin@7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)(typescript@5.4.2): + resolution: {integrity: sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: '@typescript-eslint/parser': ^7.0.0 @@ -3958,11 +3958,11 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.6.2 - '@typescript-eslint/parser': 7.1.1(eslint@8.57.0)(typescript@5.4.2) - '@typescript-eslint/scope-manager': 7.1.1 - '@typescript-eslint/type-utils': 7.1.1(eslint@8.57.0)(typescript@5.4.2) - '@typescript-eslint/utils': 7.1.1(eslint@8.57.0)(typescript@5.4.2) - '@typescript-eslint/visitor-keys': 7.1.1 + '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/scope-manager': 7.2.0 + '@typescript-eslint/type-utils': 7.2.0(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/visitor-keys': 7.2.0 debug: 4.3.4(supports-color@8.1.1) eslint: 8.57.0 graphemer: 1.4.0 @@ -3975,8 +3975,8 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@7.1.1(eslint@8.57.0)(typescript@5.4.2): - resolution: {integrity: sha512-ZWUFyL0z04R1nAEgr9e79YtV5LbafdOtN7yapNbn1ansMyaegl2D4bL7vHoJ4HPSc4CaLwuCVas8CVuneKzplQ==} + /@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.2): + resolution: {integrity: sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^8.56.0 @@ -3985,10 +3985,10 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 7.1.1 - '@typescript-eslint/types': 7.1.1 - '@typescript-eslint/typescript-estree': 7.1.1(typescript@5.4.2) - '@typescript-eslint/visitor-keys': 7.1.1 + '@typescript-eslint/scope-manager': 7.2.0 + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.2) + '@typescript-eslint/visitor-keys': 7.2.0 debug: 4.3.4(supports-color@8.1.1) eslint: 8.57.0 typescript: 5.4.2 @@ -3996,16 +3996,16 @@ packages: - supports-color dev: true - /@typescript-eslint/scope-manager@7.1.1: - resolution: {integrity: sha512-cirZpA8bJMRb4WZ+rO6+mnOJrGFDd38WoXCEI57+CYBqta8Yc8aJym2i7vyqLL1vVYljgw0X27axkUXz32T8TA==} + /@typescript-eslint/scope-manager@7.2.0: + resolution: {integrity: sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 7.1.1 - '@typescript-eslint/visitor-keys': 7.1.1 + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/visitor-keys': 7.2.0 dev: true - /@typescript-eslint/type-utils@7.1.1(eslint@8.57.0)(typescript@5.4.2): - resolution: {integrity: sha512-5r4RKze6XHEEhlZnJtR3GYeCh1IueUHdbrukV2KSlLXaTjuSfeVF8mZUVPLovidCuZfbVjfhi4c0DNSa/Rdg5g==} + /@typescript-eslint/type-utils@7.2.0(eslint@8.57.0)(typescript@5.4.2): + resolution: {integrity: sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^8.56.0 @@ -4014,8 +4014,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 7.1.1(typescript@5.4.2) - '@typescript-eslint/utils': 7.1.1(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.2) + '@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@5.4.2) debug: 4.3.4(supports-color@8.1.1) eslint: 8.57.0 ts-api-utils: 1.0.1(typescript@5.4.2) @@ -4024,13 +4024,13 @@ packages: - supports-color dev: true - /@typescript-eslint/types@7.1.1: - resolution: {integrity: sha512-KhewzrlRMrgeKm1U9bh2z5aoL4s7K3tK5DwHDn8MHv0yQfWFz/0ZR6trrIHHa5CsF83j/GgHqzdbzCXJ3crx0Q==} + /@typescript-eslint/types@7.2.0: + resolution: {integrity: sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==} engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/typescript-estree@7.1.1(typescript@5.4.2): - resolution: {integrity: sha512-9ZOncVSfr+sMXVxxca2OJOPagRwT0u/UHikM2Rd6L/aB+kL/QAuTnsv6MeXtjzCJYb8PzrXarypSGIPx3Jemxw==} + /@typescript-eslint/typescript-estree@7.2.0(typescript@5.4.2): + resolution: {integrity: sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: typescript: '*' @@ -4038,8 +4038,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 7.1.1 - '@typescript-eslint/visitor-keys': 7.1.1 + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/visitor-keys': 7.2.0 debug: 4.3.4(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 @@ -4051,8 +4051,8 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@7.1.1(eslint@8.57.0)(typescript@5.4.2): - resolution: {integrity: sha512-thOXM89xA03xAE0lW7alstvnyoBUbBX38YtY+zAUcpRPcq9EIhXPuJ0YTv948MbzmKh6e1AUszn5cBFK49Umqg==} + /@typescript-eslint/utils@7.2.0(eslint@8.57.0)(typescript@5.4.2): + resolution: {integrity: sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^8.56.0 @@ -4060,9 +4060,9 @@ packages: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@types/json-schema': 7.0.12 '@types/semver': 7.5.0 - '@typescript-eslint/scope-manager': 7.1.1 - '@typescript-eslint/types': 7.1.1 - '@typescript-eslint/typescript-estree': 7.1.1(typescript@5.4.2) + '@typescript-eslint/scope-manager': 7.2.0 + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.2) eslint: 8.57.0 semver: 7.6.0 transitivePeerDependencies: @@ -4070,11 +4070,11 @@ packages: - typescript dev: true - /@typescript-eslint/visitor-keys@7.1.1: - resolution: {integrity: sha512-yTdHDQxY7cSoCcAtiBzVzxleJhkGB9NncSIyMYe2+OGON1ZsP9zOPws/Pqgopa65jvknOjlk/w7ulPlZ78PiLQ==} + /@typescript-eslint/visitor-keys@7.2.0: + resolution: {integrity: sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 7.1.1 + '@typescript-eslint/types': 7.2.0 eslint-visitor-keys: 3.4.3 dev: true @@ -4082,7 +4082,7 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@vitejs/plugin-legacy@5.3.2(terser@5.24.0)(vite@5.1.5): + /@vitejs/plugin-legacy@5.3.2(terser@5.24.0)(vite@5.1.6): resolution: {integrity: sha512-8moCOrIMaZ/Rjln0Q6GsH6s8fAt1JOI3k8nmfX4tXUxE5KAExVctSyOBk+A25GClsdSWqIk2yaUthH3KJ2X4tg==} engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: @@ -4098,19 +4098,19 @@ packages: regenerator-runtime: 0.14.1 systemjs: 6.14.3 terser: 5.24.0 - vite: 5.1.5(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0) + vite: 5.1.6(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0) transitivePeerDependencies: - supports-color dev: true - /@vitejs/plugin-vue@5.0.4(vite@5.1.5)(vue@3.4.21): + /@vitejs/plugin-vue@5.0.4(vite@5.1.6)(vue@3.4.21): resolution: {integrity: sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==} engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: vite: ^5.0.0 vue: ^3.2.25 dependencies: - vite: 5.1.5(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0) + vite: 5.1.6(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0) vue: 3.4.21(typescript@5.4.2) dev: true @@ -4214,7 +4214,7 @@ packages: resolution: {integrity: sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==} dev: false - /@vue/eslint-config-typescript@13.0.0(eslint-plugin-vue@9.22.0)(eslint@8.57.0)(typescript@5.4.2): + /@vue/eslint-config-typescript@13.0.0(eslint-plugin-vue@9.23.0)(eslint@8.57.0)(typescript@5.4.2): resolution: {integrity: sha512-MHh9SncG/sfqjVqjcuFLOLD6Ed4dRAis4HNt0dXASeAuLqIAx4YMB1/m2o4pUKK1vCt8fUvYG8KKX2Ot3BVZTg==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: @@ -4225,10 +4225,10 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 7.1.1(@typescript-eslint/parser@7.1.1)(eslint@8.57.0)(typescript@5.4.2) - '@typescript-eslint/parser': 7.1.1(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/eslint-plugin': 7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.4.2) eslint: 8.57.0 - eslint-plugin-vue: 9.22.0(eslint@8.57.0) + eslint-plugin-vue: 9.23.0(eslint@8.57.0) typescript: 5.4.2 vue-eslint-parser: 9.4.2(eslint@8.57.0) transitivePeerDependencies: @@ -5782,8 +5782,8 @@ packages: optionalDependencies: source-map: 0.6.1 - /eslint-plugin-vue@9.22.0(eslint@8.57.0): - resolution: {integrity: sha512-7wCXv5zuVnBtZE/74z4yZ0CM8AjH6bk4MQGm7hZjUC2DBppKU5ioeOk5LGSg/s9a1ZJnIsdPLJpXnu1Rc+cVHg==} + /eslint-plugin-vue@9.23.0(eslint@8.57.0): + resolution: {integrity: sha512-Bqd/b7hGYGrlV+wP/g77tjyFmp81lh5TMw0be9093X02SyelxRRfCI6/IsGq/J7Um0YwB9s0Ry0wlFyjPdmtUw==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 @@ -6449,8 +6449,8 @@ packages: strip-bom-string: 1.0.0 dev: true - /happy-dom@13.7.3: - resolution: {integrity: sha512-xMwilTgO34BGEX0TAoM369wwwAy0fK/Jq6BGaRYhSjxLtfQ740nqxHfjFyBqPjCVmKiPUS4npBnMrGLii7eCOg==} + /happy-dom@13.7.8: + resolution: {integrity: sha512-dnvgCiPPfXXts+AW1DVAoDa9nPmI48YPHUv34L6pmjv2lwNZte8OwsK9SajEXENfibS8uo1zG7xJwlW/NXlDxQ==} engines: {node: '>=16.0.0'} dependencies: entities: 4.5.0 @@ -6517,16 +6517,16 @@ packages: engines: {node: '>=12.0.0'} dev: false - /histoire@0.17.9(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0)(vite@5.1.5): + /histoire@0.17.9(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0)(vite@5.1.6): resolution: {integrity: sha512-z5Jb9QwbOw0TKvpkU0v7+CxJG6hIljIKMhWXzOfteteRZGDFElpTEwbr5/8EdPI6VTdF/k76fqZ07nmS9YdUvA==} hasBin: true peerDependencies: vite: ^2.9.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 dependencies: '@akryum/tinypool': 0.3.1 - '@histoire/app': 0.17.9(vite@5.1.5) - '@histoire/controls': 0.17.9(vite@5.1.5) - '@histoire/shared': 0.17.9(vite@5.1.5) + '@histoire/app': 0.17.9(vite@5.1.6) + '@histoire/controls': 0.17.9(vite@5.1.6) + '@histoire/shared': 0.17.9(vite@5.1.6) '@histoire/vendors': 0.17.8 '@types/flexsearch': 0.7.6 '@types/markdown-it': 12.2.3 @@ -6553,8 +6553,8 @@ packages: sade: 1.8.1 shiki-es: 0.2.0 sirv: 2.0.3 - vite: 5.1.5(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0) - vite-node: 0.34.6(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0) + vite: 5.1.6(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0) + vite-node: 0.34.6(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0) transitivePeerDependencies: - '@types/node' - bufferutil @@ -7000,7 +7000,7 @@ packages: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.11.25 + '@types/node': 20.11.26 merge-stream: 2.0.0 supports-color: 7.2.0 dev: true @@ -8967,7 +8967,7 @@ packages: - acorn dev: true - /rollup-plugin-visualizer@5.12.0(rollup@4.12.1): + /rollup-plugin-visualizer@5.12.0(rollup@4.13.0): resolution: {integrity: sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==} engines: {node: '>=14'} hasBin: true @@ -8979,7 +8979,7 @@ packages: dependencies: open: 8.4.0 picomatch: 2.3.1 - rollup: 4.12.1 + rollup: 4.13.0 source-map: 0.7.4 yargs: 17.6.0 dev: true @@ -8992,26 +8992,26 @@ packages: fsevents: 2.3.3 dev: true - /rollup@4.12.1: - resolution: {integrity: sha512-ggqQKvx/PsB0FaWXhIvVkSWh7a/PCLQAsMjBc+nA2M8Rv2/HG0X6zvixAB7KyZBRtifBUhy5k8voQX/mRnABPg==} + /rollup@4.13.0: + resolution: {integrity: sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true dependencies: '@types/estree': 1.0.5 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.12.1 - '@rollup/rollup-android-arm64': 4.12.1 - '@rollup/rollup-darwin-arm64': 4.12.1 - '@rollup/rollup-darwin-x64': 4.12.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.12.1 - '@rollup/rollup-linux-arm64-gnu': 4.12.1 - '@rollup/rollup-linux-arm64-musl': 4.12.1 - '@rollup/rollup-linux-riscv64-gnu': 4.12.1 - '@rollup/rollup-linux-x64-gnu': 4.12.1 - '@rollup/rollup-linux-x64-musl': 4.12.1 - '@rollup/rollup-win32-arm64-msvc': 4.12.1 - '@rollup/rollup-win32-ia32-msvc': 4.12.1 - '@rollup/rollup-win32-x64-msvc': 4.12.1 + '@rollup/rollup-android-arm-eabi': 4.13.0 + '@rollup/rollup-android-arm64': 4.13.0 + '@rollup/rollup-darwin-arm64': 4.13.0 + '@rollup/rollup-darwin-x64': 4.13.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.13.0 + '@rollup/rollup-linux-arm64-gnu': 4.13.0 + '@rollup/rollup-linux-arm64-musl': 4.13.0 + '@rollup/rollup-linux-riscv64-gnu': 4.13.0 + '@rollup/rollup-linux-x64-gnu': 4.13.0 + '@rollup/rollup-linux-x64-musl': 4.13.0 + '@rollup/rollup-win32-arm64-msvc': 4.13.0 + '@rollup/rollup-win32-ia32-msvc': 4.13.0 + '@rollup/rollup-win32-x64-msvc': 4.13.0 fsevents: 2.3.3 /rope-sequence@1.3.4: @@ -9944,7 +9944,7 @@ packages: extsprintf: 1.3.0 dev: true - /vite-node@0.34.6(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0): + /vite-node@0.34.6(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0): resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==} engines: {node: '>=v14.18.0'} hasBin: true @@ -9954,7 +9954,7 @@ packages: mlly: 1.4.2 pathe: 1.1.1 picocolors: 1.0.0 - vite: 5.1.5(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0) + vite: 5.1.6(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0) transitivePeerDependencies: - '@types/node' - less @@ -9966,7 +9966,7 @@ packages: - terser dev: true - /vite-node@1.3.1(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0): + /vite-node@1.3.1(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0): resolution: {integrity: sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -9975,7 +9975,7 @@ packages: debug: 4.3.4(supports-color@8.1.1) pathe: 1.1.1 picocolors: 1.0.0 - vite: 5.1.5(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0) + vite: 5.1.6(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0) transitivePeerDependencies: - '@types/node' - less @@ -9987,17 +9987,17 @@ packages: - terser dev: true - /vite-plugin-inject-preload@1.3.3(vite@5.1.5): + /vite-plugin-inject-preload@1.3.3(vite@5.1.6): resolution: {integrity: sha512-nh5+6BZdR/iFZj6pfDR8NHxQgRELkcmM5f9ufj9X6BWXgh3x6SWNp24TfiYvhwQyOV/vrVXpo0DqNBSgppmeOQ==} engines: {node: '>=14.18.0'} peerDependencies: vite: ^3.0.0 || ^4.0.0 dependencies: mime-types: 2.1.35 - vite: 5.1.5(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0) + vite: 5.1.6(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0) dev: true - /vite-plugin-pwa@0.19.2(vite@5.1.5)(workbox-build@7.0.0)(workbox-window@7.0.0): + /vite-plugin-pwa@0.19.2(vite@5.1.6)(workbox-build@7.0.0)(workbox-window@7.0.0): resolution: {integrity: sha512-LSQJFPxCAQYbRuSyc9EbRLRqLpaBA9onIZuQFomfUYjWSgHuQLonahetDlPSC9zsxmkSEhQH8dXZN8yL978h3w==} engines: {node: '>=16.0.0'} peerDependencies: @@ -10012,21 +10012,21 @@ packages: debug: 4.3.4(supports-color@8.1.1) fast-glob: 3.3.2 pretty-bytes: 6.1.1 - vite: 5.1.5(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0) + vite: 5.1.6(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0) workbox-build: 7.0.0(acorn@8.11.2) workbox-window: 7.0.0 transitivePeerDependencies: - supports-color dev: true - /vite-plugin-sentry@1.4.0(vite@5.1.5): + /vite-plugin-sentry@1.4.0(vite@5.1.6): resolution: {integrity: sha512-Jt9AeDnh9XLjEA1pAfU0NW0jCyJE8lAXMJWZKc+SoIxZRKSY64fitDOg9Ta1G98LhPaiDKL6dhVeROUmjY/aUQ==} engines: {node: '>= 14'} peerDependencies: vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 dependencies: '@sentry/cli': 2.19.1 - vite: 5.1.5(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0) + vite: 5.1.6(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0) transitivePeerDependencies: - encoding - supports-color @@ -10041,8 +10041,8 @@ packages: vue: 3.4.21(typescript@5.4.2) dev: true - /vite@5.1.5(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0): - resolution: {integrity: sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q==} + /vite@5.1.6(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0): + resolution: {integrity: sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -10069,17 +10069,17 @@ packages: terser: optional: true dependencies: - '@types/node': 20.11.25 + '@types/node': 20.11.26 esbuild: 0.19.12 postcss: 8.4.35 - rollup: 4.12.1 + rollup: 4.13.0 sass: 1.71.1 terser: 5.24.0 optionalDependencies: fsevents: 2.3.3 dev: true - /vitest@1.3.1(@types/node@20.11.25)(happy-dom@13.7.3)(sass@1.71.1)(terser@5.24.0): + /vitest@1.3.1(@types/node@20.11.26)(happy-dom@13.7.8)(sass@1.71.1)(terser@5.24.0): resolution: {integrity: sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -10104,7 +10104,7 @@ packages: jsdom: optional: true dependencies: - '@types/node': 20.11.25 + '@types/node': 20.11.26 '@vitest/expect': 1.3.1 '@vitest/runner': 1.3.1 '@vitest/snapshot': 1.3.1 @@ -10114,7 +10114,7 @@ packages: chai: 4.3.10 debug: 4.3.4(supports-color@8.1.1) execa: 8.0.1 - happy-dom: 13.7.3 + happy-dom: 13.7.8 local-pkg: 0.5.0 magic-string: 0.30.7 pathe: 1.1.1 @@ -10123,8 +10123,8 @@ packages: strip-literal: 2.0.0 tinybench: 2.5.1 tinypool: 0.8.2 - vite: 5.1.5(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0) - vite-node: 1.3.1(@types/node@20.11.25)(sass@1.71.1)(terser@5.24.0) + vite: 5.1.6(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0) + vite-node: 1.3.1(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0) why-is-node-running: 2.2.2 transitivePeerDependencies: - less From 40bdecfe0df52bc4a057e8408f0fb59a46bce35f Mon Sep 17 00:00:00 2001 From: renovate Date: Mon, 11 Mar 2024 12:06:37 +0000 Subject: [PATCH 23/66] fix(deps): update dependency date-fns to v3.4.0 --- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index f000a5356..2e13ebb2d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -101,7 +101,7 @@ "blurhash": "2.0.5", "bulma-css-variables": "0.9.33", "camel-case": "4.1.2", - "date-fns": "3.3.1", + "date-fns": "3.4.0", "dayjs": "1.11.10", "dompurify": "3.0.9", "fast-deep-equal": "3.1.3", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 8806e06dc..67670afed 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -164,8 +164,8 @@ dependencies: specifier: 4.1.2 version: 4.1.2 date-fns: - specifier: 3.3.1 - version: 3.3.1 + specifier: 3.4.0 + version: 3.4.0 dayjs: specifier: 1.11.10 version: 1.11.10 @@ -5335,8 +5335,8 @@ packages: whatwg-url: 11.0.0 dev: true - /date-fns@3.3.1: - resolution: {integrity: sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==} + /date-fns@3.4.0: + resolution: {integrity: sha512-Akz4R8J9MXBsOgF1QeWeCsbv6pntT5KCPjU0Q9prBxVmWJYPLhwAIsNg3b0QAdr0ttiozYLD3L/af7Ra0jqYXw==} dev: false /dayjs@1.11.10: From b9c513f681a88253391061ab30c6b8ee59e03b15 Mon Sep 17 00:00:00 2001 From: renovate Date: Tue, 12 Mar 2024 09:07:14 +0000 Subject: [PATCH 24/66] fix(deps): update sentry-javascript monorepo to v7.106.1 --- frontend/package.json | 4 +- frontend/pnpm-lock.yaml | 106 ++++++++++++++++++++-------------------- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 2e13ebb2d..b3e7ee027 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -58,8 +58,8 @@ "@infectoone/vue-ganttastic": "2.2.0", "@intlify/unplugin-vue-i18n": "3.0.1", "@kyvg/vue3-notification": "3.2.0", - "@sentry/tracing": "7.106.0", - "@sentry/vue": "7.106.0", + "@sentry/tracing": "7.106.1", + "@sentry/vue": "7.106.1", "@tiptap/core": "2.2.4", "@tiptap/extension-blockquote": "2.2.4", "@tiptap/extension-bold": "2.2.4", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 67670afed..fbb8ddca6 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -35,11 +35,11 @@ dependencies: specifier: 3.2.0 version: 3.2.0(vue@3.4.21) '@sentry/tracing': - specifier: 7.106.0 - version: 7.106.0 + specifier: 7.106.1 + version: 7.106.1 '@sentry/vue': - specifier: 7.106.0 - version: 7.106.0(vue@3.4.21) + specifier: 7.106.1 + version: 7.106.1(vue@3.4.21) '@tiptap/core': specifier: 2.2.4 version: 2.2.4(@tiptap/pm@2.2.4) @@ -3243,45 +3243,45 @@ packages: resolution: {integrity: sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA==} dev: true - /@sentry-internal/feedback@7.106.0: - resolution: {integrity: sha512-Uz6pv3SN8XORTMme5xPxP/kuho7CAA6E/pMlpMjsojjBbnwLIICu10JaEZNsF/AtEya1RcNVTyPCrtF1F3sBYA==} + /@sentry-internal/feedback@7.106.1: + resolution: {integrity: sha512-udYR7rQnnQJ0q4PP3R7lTFx7cUz3SB4ghm8T/fJzdItrk+Puv6y8VqI19SFfDgvwgStInEzE5yys6SUQcXLBtA==} engines: {node: '>=12'} dependencies: - '@sentry/core': 7.106.0 - '@sentry/types': 7.106.0 - '@sentry/utils': 7.106.0 + '@sentry/core': 7.106.1 + '@sentry/types': 7.106.1 + '@sentry/utils': 7.106.1 dev: false - /@sentry-internal/replay-canvas@7.106.0: - resolution: {integrity: sha512-59qmT6XqbwpQuK1nVmv+XFxgd80gpYNH3aqgF5BEKux23kRB02/ARR5MwYyIHgVO0JhwdGIuiTfiLVNDu+nwTQ==} + /@sentry-internal/replay-canvas@7.106.1: + resolution: {integrity: sha512-r+nhLrQuTQih93gZ08F6MLdmaoBy/bQFcVt/2ZVqe1SkDY+MxRlXxq8ydo3FfgEjMRHdody3yT1dj6E174h23w==} engines: {node: '>=12'} dependencies: - '@sentry/core': 7.106.0 - '@sentry/replay': 7.106.0 - '@sentry/types': 7.106.0 - '@sentry/utils': 7.106.0 + '@sentry/core': 7.106.1 + '@sentry/replay': 7.106.1 + '@sentry/types': 7.106.1 + '@sentry/utils': 7.106.1 dev: false - /@sentry-internal/tracing@7.106.0: - resolution: {integrity: sha512-O8Es6Sa/tP80nfl+8soNfWzeRNFcT484SvjLR8BS3pHM9KDAlwNXyoQhFr2BKNYL1irbq6UF6eku4xCnUKVmqA==} + /@sentry-internal/tracing@7.106.1: + resolution: {integrity: sha512-Ui9zSmW88jTdmNnNBLYYpNoAi31esX5/auysC3v7+SpwxIsC3AGLFvXs4EPziyz8d0F62Ji0fNQZ96ui4fO6BQ==} engines: {node: '>=8'} dependencies: - '@sentry/core': 7.106.0 - '@sentry/types': 7.106.0 - '@sentry/utils': 7.106.0 + '@sentry/core': 7.106.1 + '@sentry/types': 7.106.1 + '@sentry/utils': 7.106.1 dev: false - /@sentry/browser@7.106.0: - resolution: {integrity: sha512-OrHdw44giTtMa1DmlIUMBN4ypj1xTES9DLjq16ufK+bLqW3rWzwCuTy0sb9ZmSxc7fL2pdBlsL+sECiS+U2TEw==} + /@sentry/browser@7.106.1: + resolution: {integrity: sha512-+Yp7OUx78ZwFFYfIvOKZGjMPW7Ds3zZSO8dsMxvDRzkA9NyyAmYMZ/dNTcsGb+PssgkCasF2XA07f6WgkNW92A==} engines: {node: '>=8'} dependencies: - '@sentry-internal/feedback': 7.106.0 - '@sentry-internal/replay-canvas': 7.106.0 - '@sentry-internal/tracing': 7.106.0 - '@sentry/core': 7.106.0 - '@sentry/replay': 7.106.0 - '@sentry/types': 7.106.0 - '@sentry/utils': 7.106.0 + '@sentry-internal/feedback': 7.106.1 + '@sentry-internal/replay-canvas': 7.106.1 + '@sentry-internal/tracing': 7.106.1 + '@sentry/core': 7.106.1 + '@sentry/replay': 7.106.1 + '@sentry/types': 7.106.1 + '@sentry/utils': 7.106.1 dev: false /@sentry/cli@2.19.1: @@ -3300,53 +3300,53 @@ packages: - supports-color dev: true - /@sentry/core@7.106.0: - resolution: {integrity: sha512-Dc13XtnyFaXup2E4vCbzuG0QKAVjrJBk4qfGwvSJaTuopEaEWBs2MpK6hRzFhsz9S3T0La7c1F/62NptvTUWsQ==} + /@sentry/core@7.106.1: + resolution: {integrity: sha512-cwCd66wkbutXCI8j14JLkyod9RHtqSNfzGpx/ieBE+N786jX+Yj1DiaZJ6ZYjKQpnToipFnacEakCd9Vc9oePA==} engines: {node: '>=8'} dependencies: - '@sentry/types': 7.106.0 - '@sentry/utils': 7.106.0 + '@sentry/types': 7.106.1 + '@sentry/utils': 7.106.1 dev: false - /@sentry/replay@7.106.0: - resolution: {integrity: sha512-buaAOvOI+3pFm+76vwtxSxciBATHyR78aDjStghJZcIpFDNF31K8ZV0uP9+EUPbXHohtkTwZ86cn/P9cyY6NgA==} + /@sentry/replay@7.106.1: + resolution: {integrity: sha512-UnuY6bj7v7CVv3T1sbLHjLutSG4hzcQQj6CjEB2NUpM+QAIguFrwAcYG4U42iNg4Qeg5q4kHi1rPpdpvh6unSA==} engines: {node: '>=12'} dependencies: - '@sentry-internal/tracing': 7.106.0 - '@sentry/core': 7.106.0 - '@sentry/types': 7.106.0 - '@sentry/utils': 7.106.0 + '@sentry-internal/tracing': 7.106.1 + '@sentry/core': 7.106.1 + '@sentry/types': 7.106.1 + '@sentry/utils': 7.106.1 dev: false - /@sentry/tracing@7.106.0: - resolution: {integrity: sha512-qHlRnNLcQpj7d/tSXL9uk49fjtfgKhd1VdqOcb66m180PF1vwIxzr3yPQ7wFZa91sHqf7Xto7faax8DnaDmpoQ==} + /@sentry/tracing@7.106.1: + resolution: {integrity: sha512-oVAxUhXR41SGpAXjfp/c1cmoUQZI0NSzn6iW95fGHN0o5FBXEU0Pxet/BAG0ik1E1dH6QHnWpKef5TytE1Nqsg==} engines: {node: '>=8'} dependencies: - '@sentry-internal/tracing': 7.106.0 + '@sentry-internal/tracing': 7.106.1 dev: false - /@sentry/types@7.106.0: - resolution: {integrity: sha512-oKTkDaL6P9xJC5/zHLRemHTWboUqRYjkJNaZCN63j4kJqGy56wee4vDtDese/NWWn4U4C1QV1h+Mifm2HmDcQg==} + /@sentry/types@7.106.1: + resolution: {integrity: sha512-g3OcyAHGugBwkQP4fZYCCZqF2ng9K7yQc9FVngKq/y7PwHm84epXdYYGDGgfQOIC1d5/GMaPxmzI5IIrZexzkg==} engines: {node: '>=8'} dev: false - /@sentry/utils@7.106.0: - resolution: {integrity: sha512-bVsePsXLpFu/1sH4rpJrPcnVxW2fXXfGfGxKs6Bm+dkOMbuVTlk/KAzIbdjCDIpVlrMDJmMNEv5xgTFjgWDkjw==} + /@sentry/utils@7.106.1: + resolution: {integrity: sha512-NIeuvB9MeDwrObbi6W5xRrNTcQj8klVvwWWYQB0zotY/LDjyl+c+cZzUshFOxBTp9ljVnYzWqZ7J8x/i4baj7w==} engines: {node: '>=8'} dependencies: - '@sentry/types': 7.106.0 + '@sentry/types': 7.106.1 dev: false - /@sentry/vue@7.106.0(vue@3.4.21): - resolution: {integrity: sha512-lKXLWtH1lArkURYkWdqpxGPvHgVfxnRSvdxUsWmCZGJP40Yvui2O3cfH/QIIyf+O7XjqJFI6z6/9Ou3av2A8MQ==} + /@sentry/vue@7.106.1(vue@3.4.21): + resolution: {integrity: sha512-XHrTtepinms5ZaKnOm8efLNxMSXMQL9j3ERy8pqrCrQCCTLrj9f9MaF/OyJkEZDgRYBxAnxPFiUyP4JNN9YCLw==} engines: {node: '>=8'} peerDependencies: vue: 2.x || 3.x dependencies: - '@sentry/browser': 7.106.0 - '@sentry/core': 7.106.0 - '@sentry/types': 7.106.0 - '@sentry/utils': 7.106.0 + '@sentry/browser': 7.106.1 + '@sentry/core': 7.106.1 + '@sentry/types': 7.106.1 + '@sentry/utils': 7.106.1 vue: 3.4.21(typescript@5.4.2) dev: false From a5c51d4b1ebf0a6bde33c0004c00eca5e0321038 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 12 Mar 2024 19:25:58 +0000 Subject: [PATCH 25/66] feat: emoji reactions for tasks and comments (#2196) This PR adds reactions for tasks and comments, similar to what you can do on Gitea, GitHub, Slack and plenty of other tools. Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2196 Co-authored-by: kolaente Co-committed-by: kolaente --- docs/content/doc/usage/errors.md | 1 + frontend/package.json | 4 +- frontend/patches/@github__hotkey@3.1.0.patch | 28 +++ frontend/pnpm-lock.yaml | 31 ++- frontend/public/emojis.json | 1 + frontend/src/components/input/Reactions.vue | 193 +++++++++++++++ .../src/components/input/editor/TipTap.vue | 4 + frontend/src/components/misc/Icon.ts | 3 +- .../components/tasks/partials/comments.vue | 7 + frontend/src/i18n/lang/en.json | 6 + frontend/src/modelTypes/IReaction.ts | 14 ++ frontend/src/modelTypes/ITask.ts | 3 + frontend/src/modelTypes/ITaskComment.ts | 3 + frontend/src/models/reaction.ts | 14 ++ frontend/src/models/task.ts | 8 + frontend/src/models/taskComment.ts | 8 + frontend/src/services/abstractService.ts | 29 ++- frontend/src/services/reactions.ts | 32 +++ frontend/src/services/task.ts | 17 +- frontend/src/services/taskComment.ts | 19 ++ frontend/src/stores/tasks.ts | 1 + frontend/src/views/tasks/TaskDetailView.vue | 9 + pkg/db/db.go | 11 + pkg/db/fixtures/reactions.yml | 6 + pkg/db/fixtures/task_comments.yml | 6 + pkg/integrations/task_collection_test.go | 30 +-- pkg/migration/20240311173251.go | 50 ++++ pkg/models/error.go | 27 +++ pkg/models/models.go | 1 + pkg/models/project.go | 6 +- pkg/models/reaction.go | 191 +++++++++++++++ pkg/models/reaction_rights.go | 81 +++++++ pkg/models/reaction_test.go | 217 +++++++++++++++++ pkg/models/task_collection_test.go | 3 + pkg/models/task_comments.go | 15 +- pkg/models/tasks.go | 13 ++ pkg/models/unit_tests.go | 1 + pkg/modules/migration/trello/trello.go | 1 + pkg/routes/routes.go | 9 + pkg/swagger/docs.go | 219 +++++++++++++++++- pkg/swagger/swagger.json | 214 +++++++++++++++++ pkg/swagger/swagger.yaml | 143 ++++++++++++ pkg/user/user.go | 11 +- 43 files changed, 1653 insertions(+), 37 deletions(-) create mode 100644 frontend/patches/@github__hotkey@3.1.0.patch create mode 100644 frontend/public/emojis.json create mode 100644 frontend/src/components/input/Reactions.vue create mode 100644 frontend/src/modelTypes/IReaction.ts create mode 100644 frontend/src/models/reaction.ts create mode 100644 frontend/src/services/reactions.ts create mode 100644 pkg/db/fixtures/reactions.yml create mode 100644 pkg/migration/20240311173251.go create mode 100644 pkg/models/reaction.go create mode 100644 pkg/models/reaction_rights.go create mode 100644 pkg/models/reaction_test.go diff --git a/docs/content/doc/usage/errors.md b/docs/content/doc/usage/errors.md index fc364ee7e..4aff46443 100644 --- a/docs/content/doc/usage/errors.md +++ b/docs/content/doc/usage/errors.md @@ -97,6 +97,7 @@ This document describes the different errors Vikunja can return. | 4022 | 400 | The task has a relative reminder which does not specify relative to what. | | 4023 | 409 | Tried to create a task relation which would create a cycle. | | 4024 | 400 | The provided filter expression is invalid. | +| 4025 | 400 | The reaction kind is invalid. | ## Team diff --git a/frontend/package.json b/frontend/package.json index b3e7ee027..424c68f46 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -123,6 +123,7 @@ "vue-flatpickr-component": "11.0.5", "vue-i18n": "9.10.1", "vue-router": "4.3.0", + "vuemoji-picker": "^0.2.1", "workbox-precaching": "7.0.0", "zhyswan-vuedraggable": "4.1.3" }, @@ -184,7 +185,8 @@ }, "pnpm": { "patchedDependencies": { - "flexsearch@0.7.31": "patches/flexsearch@0.7.31.patch" + "flexsearch@0.7.31": "patches/flexsearch@0.7.31.patch", + "@github/hotkey@3.1.0": "patches/@github__hotkey@3.1.0.patch" } } } diff --git a/frontend/patches/@github__hotkey@3.1.0.patch b/frontend/patches/@github__hotkey@3.1.0.patch new file mode 100644 index 000000000..da4b16547 --- /dev/null +++ b/frontend/patches/@github__hotkey@3.1.0.patch @@ -0,0 +1,28 @@ +diff --git a/dist/index.js b/dist/index.js +index b6e6e0a6864cb00bc085b8d4503a705cb3bc8404..0466ef46406b0df41c8d0bb9a5bac9eabf4a50de 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -368,10 +368,12 @@ const sequenceTracker = new SequenceTracker({ + function keyDownHandler(event) { + if (event.defaultPrevented) + return; +- if (!(event.target instanceof Node)) ++ const target = event.explicitOriginalTarget || event.target; ++ if (target.shadowRoot) + return; +- if (isFormField(event.target)) { +- const target = event.target; ++ if (!(target instanceof Node)) ++ return; ++ if (isFormField(target)) { + if (!target.id) + return; + if (!target.ownerDocument.querySelector(`[data-hotkey-scope="${target.id}"]`)) +@@ -385,7 +387,6 @@ function keyDownHandler(event) { + sequenceTracker.registerKeypress(event); + currentTriePosition = newTriePosition; + if (newTriePosition instanceof Leaf) { +- const target = event.target; + let shouldFire = false; + let elementToFire; + const formField = isFormField(target); diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index fbb8ddca6..ff01839a5 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false patchedDependencies: + '@github/hotkey@3.1.0': + hash: c67tdk7qpd5grxd2zj6lsxfbou + path: patches/@github__hotkey@3.1.0.patch flexsearch@0.7.31: hash: bfn3sngfuhktmdj7jgl3ejl35y path: patches/flexsearch@0.7.31.patch @@ -24,7 +27,7 @@ dependencies: version: 3.0.6(@fortawesome/fontawesome-svg-core@6.5.1)(vue@3.4.21) '@github/hotkey': specifier: 3.1.0 - version: 3.1.0 + version: 3.1.0(patch_hash=c67tdk7qpd5grxd2zj6lsxfbou) '@infectoone/vue-ganttastic': specifier: 2.2.0 version: 2.2.0(dayjs@1.11.10)(vue@3.4.21) @@ -229,6 +232,9 @@ dependencies: vue-router: specifier: 4.3.0 version: 4.3.0(vue@3.4.21) + vuemoji-picker: + specifier: ^0.2.1 + version: 0.2.1(vue@3.4.21) workbox-precaching: specifier: 7.0.0 version: 7.0.0 @@ -2687,9 +2693,10 @@ packages: vue: 3.4.21(typescript@5.4.2) dev: false - /@github/hotkey@3.1.0: + /@github/hotkey@3.1.0(patch_hash=c67tdk7qpd5grxd2zj6lsxfbou): resolution: {integrity: sha512-Lj9QjYa+b+Nk5U1nZtlXLdx3HI8/EeM6ZNwBjpYcGVYqpwHdM2ScRH0p7+5zh28JG6SPbTM9+Rb1dFd742qMTw==} dev: false + patched: true /@hapi/hoek@9.2.1: resolution: {integrity: sha512-gfta+H8aziZsm8pZa0vj04KO6biEiisppNgA1kbJvFrrWu9Vm7eaUEy76DIxsuTaWvti5fkJVhllWc6ZTE+Mdw==} @@ -4358,7 +4365,7 @@ packages: /@vueuse/shared@9.13.0(vue@3.4.21): resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==} dependencies: - vue-demi: 0.14.6(vue@3.4.21) + vue-demi: 0.14.7(vue@3.4.21) transitivePeerDependencies: - '@vue/composition-api' - vue @@ -5610,6 +5617,10 @@ packages: resolution: {integrity: sha512-yDYeobbTEe4TNooEzOQO6xFqg9XnAkVy2Lod1C1B2it8u47JNLYvl9nLDWBamqUakWB8Jc1hhS1uHUNYTNQdfw==} dev: true + /emoji-picker-element@1.21.1: + resolution: {integrity: sha512-XO3buLicIjIb59dy3R2PVzpyxUEye7DSmHApbxFJxK8gCFPlGKP/Pld8ccWNYvny9t6vYhnKP1FNYgqqMy1XHA==} + dev: false + /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} dev: true @@ -10273,6 +10284,20 @@ packages: '@vue/shared': 3.4.21 typescript: 5.4.2 + /vuemoji-picker@0.2.1(vue@3.4.21): + resolution: {integrity: sha512-wKRZBZclTdnQIT4jPzmkJ5Ci9ObzMFPjkuYb+/+/9h+mAZIUwdcPqYbEJCohbxJPoOvkuPVDeuOdTKR8hqqVLA==} + peerDependencies: + '@vue/composition-api': ^1.7.0 + vue: ^2.6.14 || ^3.2.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + dependencies: + emoji-picker-element: 1.21.1 + vue: 3.4.21(typescript@5.4.2) + vue-demi: 0.14.7(vue@3.4.21) + dev: false + /w3c-keyname@2.2.6: resolution: {integrity: sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==} diff --git a/frontend/public/emojis.json b/frontend/public/emojis.json new file mode 100644 index 000000000..aee19111a --- /dev/null +++ b/frontend/public/emojis.json @@ -0,0 +1 @@ +[{"shortcodes":["grinning","grinning_face"],"annotation":"grinning face","tags":["face","grin"],"emoji":"😀","order":1,"group":0,"version":1},{"shortcodes":["grinning_face_with_big_eyes","smiley"],"annotation":"grinning face with big eyes","tags":["face","mouth","open","smile"],"emoji":"😃","order":2,"group":0,"version":0.6},{"shortcodes":["grinning_face_with_closed_eyes","smile"],"annotation":"grinning face with smiling eyes","tags":["eye","face","mouth","open","smile"],"emoji":"😄","order":3,"group":0,"version":0.6,"emoticon":":D"},{"shortcodes":["beaming_face","grin"],"annotation":"beaming face with smiling eyes","tags":["eye","face","grin","smile"],"emoji":"😁","order":4,"group":0,"version":0.6},{"shortcodes":["laughing","lol","satisfied","squinting_face"],"annotation":"grinning squinting face","tags":["face","laugh","mouth","satisfied","smile"],"emoji":"😆","order":5,"group":0,"version":0.6,"emoticon":"XD"},{"shortcodes":["grinning_face_with_sweat","sweat_smile"],"annotation":"grinning face with sweat","tags":["cold","face","open","smile","sweat"],"emoji":"😅","order":6,"group":0,"version":0.6},{"shortcodes":["rofl"],"annotation":"rolling on the floor laughing","tags":["face","floor","laugh","rofl","rolling","rotfl"],"emoji":"🤣","order":7,"group":0,"version":3,"emoticon":":'D"},{"shortcodes":["joy","lmao","tears_of_joy"],"annotation":"face with tears of joy","tags":["face","joy","laugh","tear"],"emoji":"😂","order":8,"group":0,"version":0.6,"emoticon":":')"},{"shortcodes":["slightly_smiling_face"],"annotation":"slightly smiling face","tags":["face","smile"],"emoji":"🙂","order":9,"group":0,"version":1,"emoticon":":)"},{"shortcodes":["upside_down_face"],"annotation":"upside-down face","tags":["face","upside-down"],"emoji":"🙃","order":10,"group":0,"version":1},{"shortcodes":["melt","melting_face"],"annotation":"melting face","tags":["disappear","dissolve","liquid","melt"],"emoji":"🫠","order":11,"group":0,"version":14},{"shortcodes":["wink","winking_face"],"annotation":"winking face","tags":["face","wink"],"emoji":"😉","order":12,"group":0,"version":0.6,"emoticon":";)"},{"shortcodes":["blush","smiling_face_with_closed_eyes"],"annotation":"smiling face with smiling eyes","tags":["blush","eye","face","smile"],"emoji":"😊","order":13,"group":0,"version":0.6,"emoticon":":>"},{"shortcodes":["halo","innocent"],"annotation":"smiling face with halo","tags":["angel","face","fantasy","halo","innocent"],"emoji":"😇","order":14,"group":0,"version":1,"emoticon":"O:)"},{"shortcodes":["smiling_face_with_3_hearts"],"annotation":"smiling face with hearts","tags":["adore","crush","hearts","in love"],"emoji":"🥰","order":15,"group":0,"version":11},{"shortcodes":["heart_eyes","smiling_face_with_heart_eyes"],"annotation":"smiling face with heart-eyes","tags":["eye","face","love","smile"],"emoji":"😍","order":16,"group":0,"version":0.6},{"shortcodes":["star_struck"],"annotation":"star-struck","tags":["eyes","face","grinning","star"],"emoji":"🤩","order":17,"group":0,"version":5},{"shortcodes":["blowing_a_kiss","kissing_heart"],"annotation":"face blowing a kiss","tags":["face","kiss"],"emoji":"😘","order":18,"group":0,"version":0.6,"emoticon":":X"},{"shortcodes":["kissing","kissing_face"],"annotation":"kissing face","tags":["face","kiss"],"emoji":"😗","order":19,"group":0,"version":1},{"shortcodes":["relaxed","smiling_face"],"annotation":"smiling face","tags":["face","outlined","relaxed","smile"],"emoji":"☺️","order":21,"group":0,"version":0.6},{"shortcodes":["kissing_closed_eyes","kissing_face_with_closed_eyes"],"annotation":"kissing face with closed eyes","tags":["closed","eye","face","kiss"],"emoji":"😚","order":22,"group":0,"version":0.6,"emoticon":":*"},{"shortcodes":["kissing_face_with_smiling_eyes","kissing_smiling_eyes"],"annotation":"kissing face with smiling eyes","tags":["eye","face","kiss","smile"],"emoji":"😙","order":23,"group":0,"version":1},{"shortcodes":["smiling_face_with_tear"],"annotation":"smiling face with tear","tags":["grateful","proud","relieved","smiling","tear","touched"],"emoji":"🥲","order":24,"group":0,"version":13},{"shortcodes":["savoring_food","yum"],"annotation":"face savoring food","tags":["delicious","face","savouring","smile","yum"],"emoji":"😋","order":25,"group":0,"version":0.6},{"shortcodes":["face_with_tongue","stuck_out_tongue"],"annotation":"face with tongue","tags":["face","tongue"],"emoji":"😛","order":26,"group":0,"version":1,"emoticon":":P"},{"shortcodes":["stuck_out_tongue_winking_eye"],"annotation":"winking face with tongue","tags":["eye","face","joke","tongue","wink"],"emoji":"😜","order":27,"group":0,"version":0.6,"emoticon":";P"},{"shortcodes":["zany","zany_face"],"annotation":"zany face","tags":["eye","goofy","large","small"],"emoji":"🤪","order":28,"group":0,"version":5},{"shortcodes":["stuck_out_tongue_closed_eyes"],"annotation":"squinting face with tongue","tags":["eye","face","horrible","taste","tongue"],"emoji":"😝","order":29,"group":0,"version":0.6,"emoticon":"XP"},{"shortcodes":["money_mouth_face"],"annotation":"money-mouth face","tags":["face","money","mouth"],"emoji":"🤑","order":30,"group":0,"version":1},{"shortcodes":["hug","hugging","hugging_face"],"annotation":"smiling face with open hands","tags":["face","hug","hugging","open hands","smiling face"],"emoji":"🤗","order":31,"group":0,"version":1},{"shortcodes":["face_with_hand_over_mouth","hand_over_mouth"],"annotation":"face with hand over mouth","tags":["whoops"],"emoji":"🤭","order":32,"group":0,"version":5},{"shortcodes":["face_with_open_eyes_hand_over_mouth","gasp"],"annotation":"face with open eyes and hand over mouth","tags":["amazement","awe","disbelief","embarrass","scared","surprise"],"emoji":"🫢","order":33,"group":0,"version":14},{"shortcodes":["face_with_peeking_eye","peek"],"annotation":"face with peeking eye","tags":["captivated","peep","stare"],"emoji":"🫣","order":34,"group":0,"version":14},{"shortcodes":["shush","shushing_face"],"annotation":"shushing face","tags":["quiet","shush"],"emoji":"🤫","order":35,"group":0,"version":5},{"shortcodes":["thinking","thinking_face","wtf"],"annotation":"thinking face","tags":["face","thinking"],"emoji":"🤔","order":36,"group":0,"version":1,"emoticon":":L"},{"shortcodes":["salute","saluting_face"],"annotation":"saluting face","tags":["ok","salute","sunny","troops","yes"],"emoji":"🫡","order":37,"group":0,"version":14},{"shortcodes":["zipper_mouth","zipper_mouth_face"],"annotation":"zipper-mouth face","tags":["face","mouth","zip","zipper"],"emoji":"🤐","order":38,"group":0,"version":1,"emoticon":":Z"},{"shortcodes":["face_with_raised_eyebrow","raised_eyebrow"],"annotation":"face with raised eyebrow","tags":["distrust","skeptic"],"emoji":"🤨","order":39,"group":0,"version":5},{"shortcodes":["neutral","neutral_face"],"annotation":"neutral face","tags":["deadpan","face","meh","neutral"],"emoji":"😐️","order":40,"group":0,"version":0.7,"emoticon":":|"},{"shortcodes":["expressionless","expressionless_face"],"annotation":"expressionless face","tags":["expressionless","face","inexpressive","meh","unexpressive"],"emoji":"😑","order":41,"group":0,"version":1},{"shortcodes":["no_mouth"],"annotation":"face without mouth","tags":["face","mouth","quiet","silent"],"emoji":"😶","order":42,"group":0,"version":1,"emoticon":":#"},{"shortcodes":["dotted_line_face"],"annotation":"dotted line face","tags":["depressed","disappear","hide","introvert","invisible"],"emoji":"🫥","order":43,"group":0,"version":14},{"shortcodes":["in_clouds"],"annotation":"face in clouds","tags":["absentminded","face in the fog","head in clouds"],"emoji":"😶‍🌫️","order":44,"group":0,"version":13.1},{"shortcodes":["smirk","smirking","smirking_face"],"annotation":"smirking face","tags":["face","smirk"],"emoji":"😏","order":46,"group":0,"version":0.6,"emoticon":":j"},{"shortcodes":["unamused","unamused_face"],"annotation":"unamused face","tags":["face","unamused","unhappy"],"emoji":"😒","order":47,"group":0,"version":0.6,"emoticon":":?"},{"shortcodes":["rolling_eyes"],"annotation":"face with rolling eyes","tags":["eyeroll","eyes","face","rolling"],"emoji":"🙄","order":48,"group":0,"version":1},{"shortcodes":["grimacing","grimacing_face"],"annotation":"grimacing face","tags":["face","grimace"],"emoji":"😬","order":49,"group":0,"version":1,"emoticon":"8D"},{"shortcodes":["exhale","exhaling"],"annotation":"face exhaling","tags":["exhale","gasp","groan","relief","whisper","whistle"],"emoji":"😮‍💨","order":50,"group":0,"version":13.1},{"shortcodes":["lying","lying_face"],"annotation":"lying face","tags":["face","lie","pinocchio"],"emoji":"🤥","order":51,"group":0,"version":3},{"shortcodes":["shaking","shaking_face"],"annotation":"shaking face","tags":["earthquake","face","shaking","shock","vibrate"],"emoji":"🫨","order":52,"group":0,"version":15},{"shortcodes":["head_shaking_horizontally"],"annotation":"head shaking horizontally","tags":["no","shake"],"emoji":"🙂‍↔️","order":53,"group":0,"version":15.1},{"shortcodes":["head_shaking_vertically"],"annotation":"head shaking vertically","tags":["nod","yes"],"emoji":"🙂‍↕️","order":55,"group":0,"version":15.1},{"shortcodes":["relieved","relieved_face"],"annotation":"relieved face","tags":["face","relieved"],"emoji":"😌","order":57,"group":0,"version":0.6},{"shortcodes":["pensive","pensive_face"],"annotation":"pensive face","tags":["dejected","face","pensive"],"emoji":"😔","order":58,"group":0,"version":0.6},{"shortcodes":["sleepy","sleepy_face"],"annotation":"sleepy face","tags":["face","good night","sleep"],"emoji":"😪","order":59,"group":0,"version":0.6},{"shortcodes":["drooling","drooling_face"],"annotation":"drooling face","tags":["drooling","face"],"emoji":"🤤","order":60,"group":0,"version":3},{"shortcodes":["sleeping","sleeping_face"],"annotation":"sleeping face","tags":["face","good night","sleep","zzz"],"emoji":"😴","order":61,"group":0,"version":1},{"shortcodes":["mask","medical_mask"],"annotation":"face with medical mask","tags":["cold","doctor","face","mask","sick"],"emoji":"😷","order":62,"group":0,"version":0.6},{"shortcodes":["face_with_thermometer"],"annotation":"face with thermometer","tags":["face","ill","sick","thermometer"],"emoji":"🤒","order":63,"group":0,"version":1},{"shortcodes":["face_with_head_bandage"],"annotation":"face with head-bandage","tags":["bandage","face","hurt","injury"],"emoji":"🤕","order":64,"group":0,"version":1},{"shortcodes":["nauseated","nauseated_face"],"annotation":"nauseated face","tags":["face","nauseated","vomit"],"emoji":"🤢","order":65,"group":0,"version":3,"emoticon":"%("},{"shortcodes":["face_vomiting","vomiting"],"annotation":"face vomiting","tags":["puke","sick","vomit"],"emoji":"🤮","order":66,"group":0,"version":5},{"shortcodes":["sneezing","sneezing_face"],"annotation":"sneezing face","tags":["face","gesundheit","sneeze"],"emoji":"🤧","order":67,"group":0,"version":3},{"shortcodes":["hot","hot_face"],"annotation":"hot face","tags":["feverish","heat stroke","hot","red-faced","sweating"],"emoji":"🥵","order":68,"group":0,"version":11},{"shortcodes":["cold","cold_face"],"annotation":"cold face","tags":["blue-faced","cold","freezing","frostbite","icicles"],"emoji":"🥶","order":69,"group":0,"version":11},{"shortcodes":["woozy","woozy_face"],"annotation":"woozy face","tags":["dizzy","intoxicated","tipsy","uneven eyes","wavy mouth"],"emoji":"🥴","order":70,"group":0,"version":11,"emoticon":":&"},{"shortcodes":["dizzy_face","knocked_out"],"annotation":"face with crossed-out eyes","tags":["crossed-out eyes","dead","face","knocked out"],"emoji":"😵","order":71,"group":0,"version":0.6,"emoticon":"XO"},{"shortcodes":["dizzy_eyes"],"annotation":"face with spiral eyes","tags":["dizzy","hypnotized","spiral","trouble","whoa"],"emoji":"😵‍💫","order":72,"group":0,"version":13.1},{"shortcodes":["exploding_head"],"annotation":"exploding head","tags":["mind blown","shocked"],"emoji":"🤯","order":73,"group":0,"version":5},{"shortcodes":["cowboy","cowboy_face"],"annotation":"cowboy hat face","tags":["cowboy","cowgirl","face","hat"],"emoji":"🤠","order":74,"group":0,"version":3},{"shortcodes":["hooray","partying","partying_face"],"annotation":"partying face","tags":["celebration","hat","horn","party"],"emoji":"🥳","order":75,"group":0,"version":11},{"shortcodes":["disguised","disguised_face"],"annotation":"disguised face","tags":["disguise","face","glasses","incognito","nose"],"emoji":"🥸","order":76,"group":0,"version":13},{"shortcodes":["smiling_face_with_sunglasses","sunglasses_cool","too_cool"],"annotation":"smiling face with sunglasses","tags":["bright","cool","face","sun","sunglasses"],"emoji":"😎","order":77,"group":0,"version":1,"emoticon":"8)"},{"shortcodes":["nerd","nerd_face"],"annotation":"nerd face","tags":["face","geek","nerd"],"emoji":"🤓","order":78,"group":0,"version":1,"emoticon":":B"},{"shortcodes":["face_with_monocle"],"annotation":"face with monocle","tags":["face","monocle","stuffy"],"emoji":"🧐","order":79,"group":0,"version":5},{"shortcodes":["confused","confused_face"],"annotation":"confused face","tags":["confused","face","meh"],"emoji":"😕","order":80,"group":0,"version":1,"emoticon":":/"},{"shortcodes":["face_with_diagonal_mouth"],"annotation":"face with diagonal mouth","tags":["disappointed","meh","skeptical","unsure"],"emoji":"🫤","order":81,"group":0,"version":14},{"shortcodes":["worried","worried_face"],"annotation":"worried face","tags":["face","worried"],"emoji":"😟","order":82,"group":0,"version":1},{"shortcodes":["slightly_frowning_face"],"annotation":"slightly frowning face","tags":["face","frown"],"emoji":"🙁","order":83,"group":0,"version":1},{"shortcodes":["white_frowning_face"],"annotation":"frowning face","tags":["face","frown"],"emoji":"☹️","order":85,"group":0,"version":0.7,"emoticon":":("},{"shortcodes":["face_with_open_mouth","open_mouth"],"annotation":"face with open mouth","tags":["face","mouth","open","sympathy"],"emoji":"😮","order":86,"group":0,"version":1},{"shortcodes":["hushed","hushed_face"],"annotation":"hushed face","tags":["face","hushed","stunned","surprised"],"emoji":"😯","order":87,"group":0,"version":1},{"shortcodes":["astonished","astonished_face"],"annotation":"astonished face","tags":["astonished","face","shocked","totally"],"emoji":"😲","order":88,"group":0,"version":0.6,"emoticon":":O"},{"shortcodes":["flushed","flushed_face"],"annotation":"flushed face","tags":["dazed","face","flushed"],"emoji":"😳","order":89,"group":0,"version":0.6,"emoticon":":$"},{"shortcodes":["pleading","pleading_face"],"annotation":"pleading face","tags":["begging","mercy","puppy eyes"],"emoji":"🥺","order":90,"group":0,"version":11},{"shortcodes":["face_holding_back_tears","watery_eyes"],"annotation":"face holding back tears","tags":["angry","cry","proud","resist","sad"],"emoji":"🥹","order":91,"group":0,"version":14},{"shortcodes":["frowning","frowning_face"],"annotation":"frowning face with open mouth","tags":["face","frown","mouth","open"],"emoji":"😦","order":92,"group":0,"version":1},{"shortcodes":["anguished","anguished_face"],"annotation":"anguished face","tags":["anguished","face"],"emoji":"😧","order":93,"group":0,"version":1,"emoticon":":S"},{"shortcodes":["fearful","fearful_face"],"annotation":"fearful face","tags":["face","fear","fearful","scared"],"emoji":"😨","order":94,"group":0,"version":0.6},{"shortcodes":["anxious","anxious_face","cold_sweat"],"annotation":"anxious face with sweat","tags":["blue","cold","face","rushed","sweat"],"emoji":"😰","order":95,"group":0,"version":0.6},{"shortcodes":["disappointed_relieved","sad_relieved_face"],"annotation":"sad but relieved face","tags":["disappointed","face","relieved","whew"],"emoji":"😥","order":96,"group":0,"version":0.6},{"shortcodes":["cry","crying_face"],"annotation":"crying face","tags":["cry","face","sad","tear"],"emoji":"😢","order":97,"group":0,"version":0.6,"emoticon":":'("},{"shortcodes":["loudly_crying_face","sob"],"annotation":"loudly crying face","tags":["cry","face","sad","sob","tear"],"emoji":"😭","order":98,"group":0,"version":0.6,"emoticon":":'o"},{"shortcodes":["scream","screaming_in_fear"],"annotation":"face screaming in fear","tags":["face","fear","munch","scared","scream"],"emoji":"😱","order":99,"group":0,"version":0.6,"emoticon":"Dx"},{"shortcodes":["confounded","confounded_face"],"annotation":"confounded face","tags":["confounded","face"],"emoji":"😖","order":100,"group":0,"version":0.6,"emoticon":"X("},{"shortcodes":["persevere","persevering_face"],"annotation":"persevering face","tags":["face","persevere"],"emoji":"😣","order":101,"group":0,"version":0.6},{"shortcodes":["disappointed","disappointed_face"],"annotation":"disappointed face","tags":["disappointed","face"],"emoji":"😞","order":102,"group":0,"version":0.6},{"shortcodes":["downcast_face","sweat"],"annotation":"downcast face with sweat","tags":["cold","face","sweat"],"emoji":"😓","order":103,"group":0,"version":0.6,"emoticon":":<"},{"shortcodes":["weary","weary_face"],"annotation":"weary face","tags":["face","tired","weary"],"emoji":"😩","order":104,"group":0,"version":0.6,"emoticon":"D:"},{"shortcodes":["tired","tired_face"],"annotation":"tired face","tags":["face","tired"],"emoji":"😫","order":105,"group":0,"version":0.6,"emoticon":":C"},{"shortcodes":["yawn","yawning","yawning_face"],"annotation":"yawning face","tags":["bored","tired","yawn"],"emoji":"🥱","order":106,"group":0,"version":12},{"shortcodes":["nose_steam","triumph"],"annotation":"face with steam from nose","tags":["face","triumph","won"],"emoji":"😤","order":107,"group":0,"version":0.6},{"shortcodes":["pout","pouting_face","rage"],"annotation":"enraged face","tags":["angry","enraged","face","mad","pouting","rage","red"],"emoji":"😡","order":108,"group":0,"version":0.6,"emoticon":">:/"},{"shortcodes":["angry","angry_face"],"annotation":"angry face","tags":["anger","angry","face","mad"],"emoji":"😠","order":109,"group":0,"version":0.6},{"shortcodes":["censored","face_with_symbols_on_mouth"],"annotation":"face with symbols on mouth","tags":["swearing"],"emoji":"🤬","order":110,"group":0,"version":5,"emoticon":":@"},{"shortcodes":["smiling_imp"],"annotation":"smiling face with horns","tags":["face","fairy tale","fantasy","horns","smile"],"emoji":"😈","order":111,"group":0,"version":1,"emoticon":">:)"},{"shortcodes":["angry_imp","imp"],"annotation":"angry face with horns","tags":["demon","devil","face","fantasy","imp"],"emoji":"👿","order":112,"group":0,"version":0.6,"emoticon":">:("},{"shortcodes":["skull"],"annotation":"skull","tags":["death","face","fairy tale","monster"],"emoji":"💀","order":113,"group":0,"version":0.6},{"shortcodes":["skull_and_crossbones"],"annotation":"skull and crossbones","tags":["crossbones","death","face","monster","skull"],"emoji":"☠️","order":115,"group":0,"version":1},{"shortcodes":["poop","shit"],"annotation":"pile of poo","tags":["dung","face","monster","poo","poop"],"emoji":"💩","order":116,"group":0,"version":0.6},{"shortcodes":["clown","clown_face"],"annotation":"clown face","tags":["clown","face"],"emoji":"🤡","order":117,"group":0,"version":3},{"shortcodes":["japanese_ogre","ogre"],"annotation":"ogre","tags":["creature","face","fairy tale","fantasy","monster"],"emoji":"👹","order":118,"group":0,"version":0.6,"emoticon":">0)"},{"shortcodes":["goblin","japanese_goblin"],"annotation":"goblin","tags":["creature","face","fairy tale","fantasy","monster"],"emoji":"👺","order":119,"group":0,"version":0.6},{"shortcodes":["ghost"],"annotation":"ghost","tags":["creature","face","fairy tale","fantasy","monster"],"emoji":"👻","order":120,"group":0,"version":0.6},{"shortcodes":["alien"],"annotation":"alien","tags":["creature","extraterrestrial","face","fantasy","ufo"],"emoji":"👽️","order":121,"group":0,"version":0.6},{"shortcodes":["alien_monster","space_invader"],"annotation":"alien monster","tags":["alien","creature","extraterrestrial","face","monster","ufo"],"emoji":"👾","order":122,"group":0,"version":0.6},{"shortcodes":["robot","robot_face"],"annotation":"robot","tags":["face","monster"],"emoji":"🤖","order":123,"group":0,"version":1},{"shortcodes":["grinning_cat","smiley_cat"],"annotation":"grinning cat","tags":["cat","face","grinning","mouth","open","smile"],"emoji":"😺","order":124,"group":0,"version":0.6},{"shortcodes":["grinning_cat_with_closed_eyes","smile_cat"],"annotation":"grinning cat with smiling eyes","tags":["cat","eye","face","grin","smile"],"emoji":"😸","order":125,"group":0,"version":0.6},{"shortcodes":["joy_cat","tears_of_joy_cat"],"annotation":"cat with tears of joy","tags":["cat","face","joy","tear"],"emoji":"😹","order":126,"group":0,"version":0.6},{"shortcodes":["heart_eyes_cat","smiling_cat_with_heart_eyes"],"annotation":"smiling cat with heart-eyes","tags":["cat","eye","face","heart","love","smile"],"emoji":"😻","order":127,"group":0,"version":0.6},{"shortcodes":["smirk_cat","wry_smile_cat"],"annotation":"cat with wry smile","tags":["cat","face","ironic","smile","wry"],"emoji":"😼","order":128,"group":0,"version":0.6},{"shortcodes":["kissing_cat"],"annotation":"kissing cat","tags":["cat","eye","face","kiss"],"emoji":"😽","order":129,"group":0,"version":0.6,"emoticon":":3"},{"shortcodes":["scream_cat","weary_cat"],"annotation":"weary cat","tags":["cat","face","oh","surprised","weary"],"emoji":"🙀","order":130,"group":0,"version":0.6},{"shortcodes":["crying_cat"],"annotation":"crying cat","tags":["cat","cry","face","sad","tear"],"emoji":"😿","order":131,"group":0,"version":0.6},{"shortcodes":["pouting_cat"],"annotation":"pouting cat","tags":["cat","face","pouting"],"emoji":"😾","order":132,"group":0,"version":0.6},{"shortcodes":["see_no_evil"],"annotation":"see-no-evil monkey","tags":["evil","face","forbidden","monkey","see"],"emoji":"🙈","order":133,"group":0,"version":0.6},{"shortcodes":["hear_no_evil"],"annotation":"hear-no-evil monkey","tags":["evil","face","forbidden","hear","monkey"],"emoji":"🙉","order":134,"group":0,"version":0.6},{"shortcodes":["speak_no_evil"],"annotation":"speak-no-evil monkey","tags":["evil","face","forbidden","monkey","speak"],"emoji":"🙊","order":135,"group":0,"version":0.6},{"shortcodes":["love_letter"],"annotation":"love letter","tags":["heart","letter","love","mail"],"emoji":"💌","order":136,"group":0,"version":0.6},{"shortcodes":["cupid","heart_with_arrow"],"annotation":"heart with arrow","tags":["arrow","cupid"],"emoji":"💘","order":137,"group":0,"version":0.6},{"shortcodes":["gift_heart","heart_with_ribbon"],"annotation":"heart with ribbon","tags":["ribbon","valentine"],"emoji":"💝","order":138,"group":0,"version":0.6},{"shortcodes":["sparkling_heart"],"annotation":"sparkling heart","tags":["excited","sparkle"],"emoji":"💖","order":139,"group":0,"version":0.6},{"shortcodes":["growing_heart","heartpulse"],"annotation":"growing heart","tags":["excited","growing","nervous","pulse"],"emoji":"💗","order":140,"group":0,"version":0.6},{"shortcodes":["beating_heart","heartbeat"],"annotation":"beating heart","tags":["beating","heartbeat","pulsating"],"emoji":"💓","order":141,"group":0,"version":0.6},{"shortcodes":["revolving_hearts"],"annotation":"revolving hearts","tags":["revolving"],"emoji":"💞","order":142,"group":0,"version":0.6},{"shortcodes":["two_hearts"],"annotation":"two hearts","tags":["love"],"emoji":"💕","order":143,"group":0,"version":0.6},{"shortcodes":["heart_decoration"],"annotation":"heart decoration","tags":["heart"],"emoji":"💟","order":144,"group":0,"version":0.6},{"shortcodes":["heart_exclamation"],"annotation":"heart exclamation","tags":["exclamation","mark","punctuation"],"emoji":"❣️","order":146,"group":0,"version":1},{"shortcodes":["broken_heart"],"annotation":"broken heart","tags":["break","broken"],"emoji":"💔","order":147,"group":0,"version":0.6,"emoticon":"","skins":[{"emoji":"🧙🏻‍♂️","version":5,"tone":1},{"emoji":"🧙🏼‍♂️","version":5,"tone":2},{"emoji":"🧙🏽‍♂️","version":5,"tone":3},{"emoji":"🧙🏾‍♂️","version":5,"tone":4},{"emoji":"🧙🏿‍♂️","version":5,"tone":5}]},{"shortcodes":["woman_mage"],"annotation":"woman mage","tags":["sorceress","witch"],"emoji":"🧙‍♀️","order":1745,"group":1,"version":5,"skins":[{"emoji":"🧙🏻‍♀️","version":5,"tone":1},{"emoji":"🧙🏼‍♀️","version":5,"tone":2},{"emoji":"🧙🏽‍♀️","version":5,"tone":3},{"emoji":"🧙🏾‍♀️","version":5,"tone":4},{"emoji":"🧙🏿‍♀️","version":5,"tone":5}]},{"shortcodes":["fairy"],"annotation":"fairy","tags":["oberon","puck","titania"],"emoji":"🧚","order":1757,"group":1,"version":5,"skins":[{"emoji":"🧚🏻","version":5,"tone":1},{"emoji":"🧚🏼","version":5,"tone":2},{"emoji":"🧚🏽","version":5,"tone":3},{"emoji":"🧚🏾","version":5,"tone":4},{"emoji":"🧚🏿","version":5,"tone":5}]},{"shortcodes":["man_fairy"],"annotation":"man fairy","tags":["oberon","puck"],"emoji":"🧚‍♂️","order":1763,"group":1,"version":5,"skins":[{"emoji":"🧚🏻‍♂️","version":5,"tone":1},{"emoji":"🧚🏼‍♂️","version":5,"tone":2},{"emoji":"🧚🏽‍♂️","version":5,"tone":3},{"emoji":"🧚🏾‍♂️","version":5,"tone":4},{"emoji":"🧚🏿‍♂️","version":5,"tone":5}]},{"shortcodes":["woman_fairy"],"annotation":"woman fairy","tags":["titania"],"emoji":"🧚‍♀️","order":1775,"group":1,"version":5,"skins":[{"emoji":"🧚🏻‍♀️","version":5,"tone":1},{"emoji":"🧚🏼‍♀️","version":5,"tone":2},{"emoji":"🧚🏽‍♀️","version":5,"tone":3},{"emoji":"🧚🏾‍♀️","version":5,"tone":4},{"emoji":"🧚🏿‍♀️","version":5,"tone":5}]},{"shortcodes":["vampire"],"annotation":"vampire","tags":["dracula","undead"],"emoji":"🧛","order":1787,"group":1,"version":5,"emoticon":":E","skins":[{"emoji":"🧛🏻","version":5,"tone":1},{"emoji":"🧛🏼","version":5,"tone":2},{"emoji":"🧛🏽","version":5,"tone":3},{"emoji":"🧛🏾","version":5,"tone":4},{"emoji":"🧛🏿","version":5,"tone":5}]},{"shortcodes":["man_vampire"],"annotation":"man vampire","tags":["dracula","undead"],"emoji":"🧛‍♂️","order":1793,"group":1,"version":5,"skins":[{"emoji":"🧛🏻‍♂️","version":5,"tone":1},{"emoji":"🧛🏼‍♂️","version":5,"tone":2},{"emoji":"🧛🏽‍♂️","version":5,"tone":3},{"emoji":"🧛🏾‍♂️","version":5,"tone":4},{"emoji":"🧛🏿‍♂️","version":5,"tone":5}]},{"shortcodes":["woman_vampire"],"annotation":"woman vampire","tags":["undead"],"emoji":"🧛‍♀️","order":1805,"group":1,"version":5,"skins":[{"emoji":"🧛🏻‍♀️","version":5,"tone":1},{"emoji":"🧛🏼‍♀️","version":5,"tone":2},{"emoji":"🧛🏽‍♀️","version":5,"tone":3},{"emoji":"🧛🏾‍♀️","version":5,"tone":4},{"emoji":"🧛🏿‍♀️","version":5,"tone":5}]},{"shortcodes":["merperson"],"annotation":"merperson","tags":["mermaid","merman","merwoman"],"emoji":"🧜","order":1817,"group":1,"version":5,"skins":[{"emoji":"🧜🏻","version":5,"tone":1},{"emoji":"🧜🏼","version":5,"tone":2},{"emoji":"🧜🏽","version":5,"tone":3},{"emoji":"🧜🏾","version":5,"tone":4},{"emoji":"🧜🏿","version":5,"tone":5}]},{"shortcodes":["merman"],"annotation":"merman","tags":["triton"],"emoji":"🧜‍♂️","order":1823,"group":1,"version":5,"skins":[{"emoji":"🧜🏻‍♂️","version":5,"tone":1},{"emoji":"🧜🏼‍♂️","version":5,"tone":2},{"emoji":"🧜🏽‍♂️","version":5,"tone":3},{"emoji":"🧜🏾‍♂️","version":5,"tone":4},{"emoji":"🧜🏿‍♂️","version":5,"tone":5}]},{"shortcodes":["mermaid"],"annotation":"mermaid","tags":["merwoman"],"emoji":"🧜‍♀️","order":1835,"group":1,"version":5,"skins":[{"emoji":"🧜🏻‍♀️","version":5,"tone":1},{"emoji":"🧜🏼‍♀️","version":5,"tone":2},{"emoji":"🧜🏽‍♀️","version":5,"tone":3},{"emoji":"🧜🏾‍♀️","version":5,"tone":4},{"emoji":"🧜🏿‍♀️","version":5,"tone":5}]},{"shortcodes":["elf"],"annotation":"elf","tags":["magical"],"emoji":"🧝","order":1847,"group":1,"version":5,"skins":[{"emoji":"🧝🏻","version":5,"tone":1},{"emoji":"🧝🏼","version":5,"tone":2},{"emoji":"🧝🏽","version":5,"tone":3},{"emoji":"🧝🏾","version":5,"tone":4},{"emoji":"🧝🏿","version":5,"tone":5}]},{"shortcodes":["man_elf"],"annotation":"man elf","tags":["magical"],"emoji":"🧝‍♂️","order":1853,"group":1,"version":5,"skins":[{"emoji":"🧝🏻‍♂️","version":5,"tone":1},{"emoji":"🧝🏼‍♂️","version":5,"tone":2},{"emoji":"🧝🏽‍♂️","version":5,"tone":3},{"emoji":"🧝🏾‍♂️","version":5,"tone":4},{"emoji":"🧝🏿‍♂️","version":5,"tone":5}]},{"shortcodes":["woman_elf"],"annotation":"woman elf","tags":["magical"],"emoji":"🧝‍♀️","order":1865,"group":1,"version":5,"skins":[{"emoji":"🧝🏻‍♀️","version":5,"tone":1},{"emoji":"🧝🏼‍♀️","version":5,"tone":2},{"emoji":"🧝🏽‍♀️","version":5,"tone":3},{"emoji":"🧝🏾‍♀️","version":5,"tone":4},{"emoji":"🧝🏿‍♀️","version":5,"tone":5}]},{"shortcodes":["genie"],"annotation":"genie","tags":["djinn"],"emoji":"🧞","order":1877,"group":1,"version":5},{"shortcodes":["man_genie"],"annotation":"man genie","tags":["djinn"],"emoji":"🧞‍♂️","order":1878,"group":1,"version":5},{"shortcodes":["woman_genie"],"annotation":"woman genie","tags":["djinn"],"emoji":"🧞‍♀️","order":1880,"group":1,"version":5},{"shortcodes":["zombie"],"annotation":"zombie","tags":["undead","walking dead"],"emoji":"🧟","order":1882,"group":1,"version":5,"emoticon":"8#"},{"shortcodes":["man_zombie"],"annotation":"man zombie","tags":["undead","walking dead"],"emoji":"🧟‍♂️","order":1883,"group":1,"version":5},{"shortcodes":["woman_zombie"],"annotation":"woman zombie","tags":["undead","walking dead"],"emoji":"🧟‍♀️","order":1885,"group":1,"version":5},{"shortcodes":["troll"],"annotation":"troll","tags":["fairy tale","fantasy","monster"],"emoji":"🧌","order":1887,"group":1,"version":14},{"shortcodes":["massage","person_getting_massage"],"annotation":"person getting massage","tags":["face","massage","salon"],"emoji":"💆","order":1888,"group":1,"version":0.6,"skins":[{"emoji":"💆🏻","version":1,"tone":1},{"emoji":"💆🏼","version":1,"tone":2},{"emoji":"💆🏽","version":1,"tone":3},{"emoji":"💆🏾","version":1,"tone":4},{"emoji":"💆🏿","version":1,"tone":5}]},{"shortcodes":["man_getting_massage"],"annotation":"man getting massage","tags":["face","man","massage"],"emoji":"💆‍♂️","order":1894,"group":1,"version":4,"skins":[{"emoji":"💆🏻‍♂️","version":4,"tone":1},{"emoji":"💆🏼‍♂️","version":4,"tone":2},{"emoji":"💆🏽‍♂️","version":4,"tone":3},{"emoji":"💆🏾‍♂️","version":4,"tone":4},{"emoji":"💆🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_getting_massage"],"annotation":"woman getting massage","tags":["face","massage","woman"],"emoji":"💆‍♀️","order":1906,"group":1,"version":4,"skins":[{"emoji":"💆🏻‍♀️","version":4,"tone":1},{"emoji":"💆🏼‍♀️","version":4,"tone":2},{"emoji":"💆🏽‍♀️","version":4,"tone":3},{"emoji":"💆🏾‍♀️","version":4,"tone":4},{"emoji":"💆🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["haircut","person_getting_haircut"],"annotation":"person getting haircut","tags":["barber","beauty","haircut","parlor"],"emoji":"💇","order":1918,"group":1,"version":0.6,"skins":[{"emoji":"💇🏻","version":1,"tone":1},{"emoji":"💇🏼","version":1,"tone":2},{"emoji":"💇🏽","version":1,"tone":3},{"emoji":"💇🏾","version":1,"tone":4},{"emoji":"💇🏿","version":1,"tone":5}]},{"shortcodes":["man_getting_haircut"],"annotation":"man getting haircut","tags":["haircut","man"],"emoji":"💇‍♂️","order":1924,"group":1,"version":4,"skins":[{"emoji":"💇🏻‍♂️","version":4,"tone":1},{"emoji":"💇🏼‍♂️","version":4,"tone":2},{"emoji":"💇🏽‍♂️","version":4,"tone":3},{"emoji":"💇🏾‍♂️","version":4,"tone":4},{"emoji":"💇🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_getting_haircut"],"annotation":"woman getting haircut","tags":["haircut","woman"],"emoji":"💇‍♀️","order":1936,"group":1,"version":4,"skins":[{"emoji":"💇🏻‍♀️","version":4,"tone":1},{"emoji":"💇🏼‍♀️","version":4,"tone":2},{"emoji":"💇🏽‍♀️","version":4,"tone":3},{"emoji":"💇🏾‍♀️","version":4,"tone":4},{"emoji":"💇🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["person_walking","walking"],"annotation":"person walking","tags":["hike","walk","walking"],"emoji":"🚶","order":1948,"group":1,"version":0.6,"skins":[{"emoji":"🚶🏻","version":1,"tone":1},{"emoji":"🚶🏼","version":1,"tone":2},{"emoji":"🚶🏽","version":1,"tone":3},{"emoji":"🚶🏾","version":1,"tone":4},{"emoji":"🚶🏿","version":1,"tone":5}]},{"shortcodes":["man_walking"],"annotation":"man walking","tags":["hike","man","walk"],"emoji":"🚶‍♂️","order":1954,"group":1,"version":4,"skins":[{"emoji":"🚶🏻‍♂️","version":4,"tone":1},{"emoji":"🚶🏼‍♂️","version":4,"tone":2},{"emoji":"🚶🏽‍♂️","version":4,"tone":3},{"emoji":"🚶🏾‍♂️","version":4,"tone":4},{"emoji":"🚶🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_walking"],"annotation":"woman walking","tags":["hike","walk","woman"],"emoji":"🚶‍♀️","order":1966,"group":1,"version":4,"skins":[{"emoji":"🚶🏻‍♀️","version":4,"tone":1},{"emoji":"🚶🏼‍♀️","version":4,"tone":2},{"emoji":"🚶🏽‍♀️","version":4,"tone":3},{"emoji":"🚶🏾‍♀️","version":4,"tone":4},{"emoji":"🚶🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["person_walking_right"],"annotation":"person walking facing right","tags":["arrow","cardinal","direction","east","hike","person walking","right arrow","walk","walking"],"emoji":"🚶‍➡️","order":1978,"group":1,"version":15.1,"skins":[{"emoji":"🚶🏻‍➡️","version":15.1,"tone":1},{"emoji":"🚶🏼‍➡️","version":15.1,"tone":2},{"emoji":"🚶🏽‍➡️","version":15.1,"tone":3},{"emoji":"🚶🏾‍➡️","version":15.1,"tone":4},{"emoji":"🚶🏿‍➡️","version":15.1,"tone":5}]},{"shortcodes":["woman_walking_right"],"annotation":"woman walking facing right","tags":["arrow","cardinal","direction","east","female sign","hike","person walking","right arrow","walk","walking","woman"],"emoji":"🚶‍♀️‍➡️","order":1990,"group":1,"version":15.1,"skins":[{"emoji":"🚶🏻‍♀️‍➡️","version":15.1,"tone":1},{"emoji":"🚶🏼‍♀️‍➡️","version":15.1,"tone":2},{"emoji":"🚶🏽‍♀️‍➡️","version":15.1,"tone":3},{"emoji":"🚶🏾‍♀️‍➡️","version":15.1,"tone":4},{"emoji":"🚶🏿‍♀️‍➡️","version":15.1,"tone":5}]},{"shortcodes":["man_walking_right"],"annotation":"man walking facing right","tags":["arrow","cardinal","direction","east","hike","male sign","man","person walking","right arrow","walk","walking"],"emoji":"🚶‍♂️‍➡️","order":2014,"group":1,"version":15.1,"skins":[{"emoji":"🚶🏻‍♂️‍➡️","version":15.1,"tone":1},{"emoji":"🚶🏼‍♂️‍➡️","version":15.1,"tone":2},{"emoji":"🚶🏽‍♂️‍➡️","version":15.1,"tone":3},{"emoji":"🚶🏾‍♂️‍➡️","version":15.1,"tone":4},{"emoji":"🚶🏿‍♂️‍➡️","version":15.1,"tone":5}]},{"shortcodes":["person_standing","standing"],"annotation":"person standing","tags":["stand","standing"],"emoji":"🧍","order":2038,"group":1,"version":12,"skins":[{"emoji":"🧍🏻","version":12,"tone":1},{"emoji":"🧍🏼","version":12,"tone":2},{"emoji":"🧍🏽","version":12,"tone":3},{"emoji":"🧍🏾","version":12,"tone":4},{"emoji":"🧍🏿","version":12,"tone":5}]},{"shortcodes":["man_standing"],"annotation":"man standing","tags":["man","standing"],"emoji":"🧍‍♂️","order":2044,"group":1,"version":12,"skins":[{"emoji":"🧍🏻‍♂️","version":12,"tone":1},{"emoji":"🧍🏼‍♂️","version":12,"tone":2},{"emoji":"🧍🏽‍♂️","version":12,"tone":3},{"emoji":"🧍🏾‍♂️","version":12,"tone":4},{"emoji":"🧍🏿‍♂️","version":12,"tone":5}]},{"shortcodes":["woman_standing"],"annotation":"woman standing","tags":["standing","woman"],"emoji":"🧍‍♀️","order":2056,"group":1,"version":12,"skins":[{"emoji":"🧍🏻‍♀️","version":12,"tone":1},{"emoji":"🧍🏼‍♀️","version":12,"tone":2},{"emoji":"🧍🏽‍♀️","version":12,"tone":3},{"emoji":"🧍🏾‍♀️","version":12,"tone":4},{"emoji":"🧍🏿‍♀️","version":12,"tone":5}]},{"shortcodes":["kneeling","person_kneeling"],"annotation":"person kneeling","tags":["kneel","kneeling"],"emoji":"🧎","order":2068,"group":1,"version":12,"skins":[{"emoji":"🧎🏻","version":12,"tone":1},{"emoji":"🧎🏼","version":12,"tone":2},{"emoji":"🧎🏽","version":12,"tone":3},{"emoji":"🧎🏾","version":12,"tone":4},{"emoji":"🧎🏿","version":12,"tone":5}]},{"shortcodes":["man_kneeling"],"annotation":"man kneeling","tags":["kneeling","man"],"emoji":"🧎‍♂️","order":2074,"group":1,"version":12,"skins":[{"emoji":"🧎🏻‍♂️","version":12,"tone":1},{"emoji":"🧎🏼‍♂️","version":12,"tone":2},{"emoji":"🧎🏽‍♂️","version":12,"tone":3},{"emoji":"🧎🏾‍♂️","version":12,"tone":4},{"emoji":"🧎🏿‍♂️","version":12,"tone":5}]},{"shortcodes":["woman_kneeling"],"annotation":"woman kneeling","tags":["kneeling","woman"],"emoji":"🧎‍♀️","order":2086,"group":1,"version":12,"skins":[{"emoji":"🧎🏻‍♀️","version":12,"tone":1},{"emoji":"🧎🏼‍♀️","version":12,"tone":2},{"emoji":"🧎🏽‍♀️","version":12,"tone":3},{"emoji":"🧎🏾‍♀️","version":12,"tone":4},{"emoji":"🧎🏿‍♀️","version":12,"tone":5}]},{"shortcodes":["person_kneeling_right"],"annotation":"person kneeling facing right","tags":["arrow","cardinal","direction","east","kneel","kneeling","person kneeling","right arrow"],"emoji":"🧎‍➡️","order":2098,"group":1,"version":15.1,"skins":[{"emoji":"🧎🏻‍➡️","version":15.1,"tone":1},{"emoji":"🧎🏼‍➡️","version":15.1,"tone":2},{"emoji":"🧎🏽‍➡️","version":15.1,"tone":3},{"emoji":"🧎🏾‍➡️","version":15.1,"tone":4},{"emoji":"🧎🏿‍➡️","version":15.1,"tone":5}]},{"shortcodes":["woman_kneeling_right"],"annotation":"woman kneeling facing right","tags":["arrow","cardinal","direction","east","female sign","kneel","kneeling","person kneeling","right arrow","woman"],"emoji":"🧎‍♀️‍➡️","order":2110,"group":1,"version":15.1,"skins":[{"emoji":"🧎🏻‍♀️‍➡️","version":15.1,"tone":1},{"emoji":"🧎🏼‍♀️‍➡️","version":15.1,"tone":2},{"emoji":"🧎🏽‍♀️‍➡️","version":15.1,"tone":3},{"emoji":"🧎🏾‍♀️‍➡️","version":15.1,"tone":4},{"emoji":"🧎🏿‍♀️‍➡️","version":15.1,"tone":5}]},{"shortcodes":["man_kneeling_right"],"annotation":"man kneeling facing right","tags":["arrow","cardinal","direction","east","kneel","kneeling","male sign","man","person kneeling","right arrow"],"emoji":"🧎‍♂️‍➡️","order":2134,"group":1,"version":15.1,"skins":[{"emoji":"🧎🏻‍♂️‍➡️","version":15.1,"tone":1},{"emoji":"🧎🏼‍♂️‍➡️","version":15.1,"tone":2},{"emoji":"🧎🏽‍♂️‍➡️","version":15.1,"tone":3},{"emoji":"🧎🏾‍♂️‍➡️","version":15.1,"tone":4},{"emoji":"🧎🏿‍♂️‍➡️","version":15.1,"tone":5}]},{"shortcodes":["person_with_probing_cane","person_with_white_cane"],"annotation":"person with white cane","tags":["accessibility","blind"],"emoji":"🧑‍🦯","order":2158,"group":1,"version":12.1,"skins":[{"emoji":"🧑🏻‍🦯","version":12.1,"tone":1},{"emoji":"🧑🏼‍🦯","version":12.1,"tone":2},{"emoji":"🧑🏽‍🦯","version":12.1,"tone":3},{"emoji":"🧑🏾‍🦯","version":12.1,"tone":4},{"emoji":"🧑🏿‍🦯","version":12.1,"tone":5}]},{"shortcodes":["person_with_white_cane_right"],"annotation":"person with white cane facing right","tags":["accessibility","adult","arrow","blind","cardinal","direction","east","gender-neutral","person","right arrow","unspecified gender","white cane"],"emoji":"🧑‍🦯‍➡️","order":2164,"group":1,"version":15.1,"skins":[{"emoji":"🧑🏻‍🦯‍➡️","version":15.1,"tone":1},{"emoji":"🧑🏼‍🦯‍➡️","version":15.1,"tone":2},{"emoji":"🧑🏽‍🦯‍➡️","version":15.1,"tone":3},{"emoji":"🧑🏾‍🦯‍➡️","version":15.1,"tone":4},{"emoji":"🧑🏿‍🦯‍➡️","version":15.1,"tone":5}]},{"shortcodes":["man_with_probing_cane","man_with_white_cane"],"annotation":"man with white cane","tags":["accessibility","blind","man"],"emoji":"👨‍🦯","order":2176,"group":1,"version":12,"skins":[{"emoji":"👨🏻‍🦯","version":12,"tone":1},{"emoji":"👨🏼‍🦯","version":12,"tone":2},{"emoji":"👨🏽‍🦯","version":12,"tone":3},{"emoji":"👨🏾‍🦯","version":12,"tone":4},{"emoji":"👨🏿‍🦯","version":12,"tone":5}]},{"shortcodes":["man_with_white_cane_right"],"annotation":"man with white cane facing right","tags":["accessibility","adult","arrow","blind","cardinal","direction","east","man","right arrow","white cane"],"emoji":"👨‍🦯‍➡️","order":2182,"group":1,"version":15.1,"skins":[{"emoji":"👨🏻‍🦯‍➡️","version":15.1,"tone":1},{"emoji":"👨🏼‍🦯‍➡️","version":15.1,"tone":2},{"emoji":"👨🏽‍🦯‍➡️","version":15.1,"tone":3},{"emoji":"👨🏾‍🦯‍➡️","version":15.1,"tone":4},{"emoji":"👨🏿‍🦯‍➡️","version":15.1,"tone":5}]},{"shortcodes":["woman_with_probing_cane","woman_with_white_cane"],"annotation":"woman with white cane","tags":["accessibility","blind","woman"],"emoji":"👩‍🦯","order":2194,"group":1,"version":12,"skins":[{"emoji":"👩🏻‍🦯","version":12,"tone":1},{"emoji":"👩🏼‍🦯","version":12,"tone":2},{"emoji":"👩🏽‍🦯","version":12,"tone":3},{"emoji":"👩🏾‍🦯","version":12,"tone":4},{"emoji":"👩🏿‍🦯","version":12,"tone":5}]},{"shortcodes":["woman_with_white_cane_right"],"annotation":"woman with white cane facing right","tags":["accessibility","adult","arrow","blind","cardinal","direction","east","right arrow","white cane","woman"],"emoji":"👩‍🦯‍➡️","order":2200,"group":1,"version":15.1,"skins":[{"emoji":"👩🏻‍🦯‍➡️","version":15.1,"tone":1},{"emoji":"👩🏼‍🦯‍➡️","version":15.1,"tone":2},{"emoji":"👩🏽‍🦯‍➡️","version":15.1,"tone":3},{"emoji":"👩🏾‍🦯‍➡️","version":15.1,"tone":4},{"emoji":"👩🏿‍🦯‍➡️","version":15.1,"tone":5}]},{"shortcodes":["person_in_motorized_wheelchair"],"annotation":"person in motorized wheelchair","tags":["accessibility","wheelchair"],"emoji":"🧑‍🦼","order":2212,"group":1,"version":12.1,"skins":[{"emoji":"🧑🏻‍🦼","version":12.1,"tone":1},{"emoji":"🧑🏼‍🦼","version":12.1,"tone":2},{"emoji":"🧑🏽‍🦼","version":12.1,"tone":3},{"emoji":"🧑🏾‍🦼","version":12.1,"tone":4},{"emoji":"🧑🏿‍🦼","version":12.1,"tone":5}]},{"shortcodes":["person_in_motorized_wheelchair_right"],"annotation":"person in motorized wheelchair facing right","tags":["accessibility","adult","arrow","cardinal","direction","east","gender-neutral","motorized wheelchair","person","right arrow","unspecified gender"],"emoji":"🧑‍🦼‍➡️","order":2218,"group":1,"version":15.1,"skins":[{"emoji":"🧑🏻‍🦼‍➡️","version":15.1,"tone":1},{"emoji":"🧑🏼‍🦼‍➡️","version":15.1,"tone":2},{"emoji":"🧑🏽‍🦼‍➡️","version":15.1,"tone":3},{"emoji":"🧑🏾‍🦼‍➡️","version":15.1,"tone":4},{"emoji":"🧑🏿‍🦼‍➡️","version":15.1,"tone":5}]},{"shortcodes":["man_in_motorized_wheelchair"],"annotation":"man in motorized wheelchair","tags":["accessibility","man","wheelchair"],"emoji":"👨‍🦼","order":2230,"group":1,"version":12,"skins":[{"emoji":"👨🏻‍🦼","version":12,"tone":1},{"emoji":"👨🏼‍🦼","version":12,"tone":2},{"emoji":"👨🏽‍🦼","version":12,"tone":3},{"emoji":"👨🏾‍🦼","version":12,"tone":4},{"emoji":"👨🏿‍🦼","version":12,"tone":5}]},{"shortcodes":["man_in_motorized_wheelchair_right"],"annotation":"man in motorized wheelchair facing right","tags":["accessibility","adult","arrow","cardinal","direction","east","man","motorized wheelchair","right arrow"],"emoji":"👨‍🦼‍➡️","order":2236,"group":1,"version":15.1,"skins":[{"emoji":"👨🏻‍🦼‍➡️","version":15.1,"tone":1},{"emoji":"👨🏼‍🦼‍➡️","version":15.1,"tone":2},{"emoji":"👨🏽‍🦼‍➡️","version":15.1,"tone":3},{"emoji":"👨🏾‍🦼‍➡️","version":15.1,"tone":4},{"emoji":"👨🏿‍🦼‍➡️","version":15.1,"tone":5}]},{"shortcodes":["woman_in_motorized_wheelchair"],"annotation":"woman in motorized wheelchair","tags":["accessibility","wheelchair","woman"],"emoji":"👩‍🦼","order":2248,"group":1,"version":12,"skins":[{"emoji":"👩🏻‍🦼","version":12,"tone":1},{"emoji":"👩🏼‍🦼","version":12,"tone":2},{"emoji":"👩🏽‍🦼","version":12,"tone":3},{"emoji":"👩🏾‍🦼","version":12,"tone":4},{"emoji":"👩🏿‍🦼","version":12,"tone":5}]},{"shortcodes":["woman_in_motorized_wheelchair_right"],"annotation":"woman in motorized wheelchair facing right","tags":["accessibility","adult","arrow","cardinal","direction","east","motorized wheelchair","right arrow","woman"],"emoji":"👩‍🦼‍➡️","order":2254,"group":1,"version":15.1,"skins":[{"emoji":"👩🏻‍🦼‍➡️","version":15.1,"tone":1},{"emoji":"👩🏼‍🦼‍➡️","version":15.1,"tone":2},{"emoji":"👩🏽‍🦼‍➡️","version":15.1,"tone":3},{"emoji":"👩🏾‍🦼‍➡️","version":15.1,"tone":4},{"emoji":"👩🏿‍🦼‍➡️","version":15.1,"tone":5}]},{"shortcodes":["person_in_manual_wheelchair"],"annotation":"person in manual wheelchair","tags":["accessibility","wheelchair"],"emoji":"🧑‍🦽","order":2266,"group":1,"version":12.1,"skins":[{"emoji":"🧑🏻‍🦽","version":12.1,"tone":1},{"emoji":"🧑🏼‍🦽","version":12.1,"tone":2},{"emoji":"🧑🏽‍🦽","version":12.1,"tone":3},{"emoji":"🧑🏾‍🦽","version":12.1,"tone":4},{"emoji":"🧑🏿‍🦽","version":12.1,"tone":5}]},{"shortcodes":["person_in_manual_wheelchair_right"],"annotation":"person in manual wheelchair facing right","tags":["accessibility","adult","arrow","cardinal","direction","east","gender-neutral","manual wheelchair","person","right arrow","unspecified gender"],"emoji":"🧑‍🦽‍➡️","order":2272,"group":1,"version":15.1,"skins":[{"emoji":"🧑🏻‍🦽‍➡️","version":15.1,"tone":1},{"emoji":"🧑🏼‍🦽‍➡️","version":15.1,"tone":2},{"emoji":"🧑🏽‍🦽‍➡️","version":15.1,"tone":3},{"emoji":"🧑🏾‍🦽‍➡️","version":15.1,"tone":4},{"emoji":"🧑🏿‍🦽‍➡️","version":15.1,"tone":5}]},{"shortcodes":["man_in_manual_wheelchair"],"annotation":"man in manual wheelchair","tags":["accessibility","man","wheelchair"],"emoji":"👨‍🦽","order":2284,"group":1,"version":12,"skins":[{"emoji":"👨🏻‍🦽","version":12,"tone":1},{"emoji":"👨🏼‍🦽","version":12,"tone":2},{"emoji":"👨🏽‍🦽","version":12,"tone":3},{"emoji":"👨🏾‍🦽","version":12,"tone":4},{"emoji":"👨🏿‍🦽","version":12,"tone":5}]},{"shortcodes":["man_in_manual_wheelchair_right"],"annotation":"man in manual wheelchair facing right","tags":["accessibility","adult","arrow","cardinal","direction","east","man","manual wheelchair","right arrow"],"emoji":"👨‍🦽‍➡️","order":2290,"group":1,"version":15.1,"skins":[{"emoji":"👨🏻‍🦽‍➡️","version":15.1,"tone":1},{"emoji":"👨🏼‍🦽‍➡️","version":15.1,"tone":2},{"emoji":"👨🏽‍🦽‍➡️","version":15.1,"tone":3},{"emoji":"👨🏾‍🦽‍➡️","version":15.1,"tone":4},{"emoji":"👨🏿‍🦽‍➡️","version":15.1,"tone":5}]},{"shortcodes":["woman_in_manual_wheelchair"],"annotation":"woman in manual wheelchair","tags":["accessibility","wheelchair","woman"],"emoji":"👩‍🦽","order":2302,"group":1,"version":12,"skins":[{"emoji":"👩🏻‍🦽","version":12,"tone":1},{"emoji":"👩🏼‍🦽","version":12,"tone":2},{"emoji":"👩🏽‍🦽","version":12,"tone":3},{"emoji":"👩🏾‍🦽","version":12,"tone":4},{"emoji":"👩🏿‍🦽","version":12,"tone":5}]},{"shortcodes":["woman_in_manual_wheelchair_right"],"annotation":"woman in manual wheelchair facing right","tags":["accessibility","adult","arrow","cardinal","direction","east","manual wheelchair","right arrow","woman"],"emoji":"👩‍🦽‍➡️","order":2308,"group":1,"version":15.1,"skins":[{"emoji":"👩🏻‍🦽‍➡️","version":15.1,"tone":1},{"emoji":"👩🏼‍🦽‍➡️","version":15.1,"tone":2},{"emoji":"👩🏽‍🦽‍➡️","version":15.1,"tone":3},{"emoji":"👩🏾‍🦽‍➡️","version":15.1,"tone":4},{"emoji":"👩🏿‍🦽‍➡️","version":15.1,"tone":5}]},{"shortcodes":["person_running","running"],"annotation":"person running","tags":["marathon","running"],"emoji":"🏃","order":2320,"group":1,"version":0.6,"skins":[{"emoji":"🏃🏻","version":1,"tone":1},{"emoji":"🏃🏼","version":1,"tone":2},{"emoji":"🏃🏽","version":1,"tone":3},{"emoji":"🏃🏾","version":1,"tone":4},{"emoji":"🏃🏿","version":1,"tone":5}]},{"shortcodes":["man_running"],"annotation":"man running","tags":["man","marathon","racing","running"],"emoji":"🏃‍♂️","order":2326,"group":1,"version":4,"skins":[{"emoji":"🏃🏻‍♂️","version":4,"tone":1},{"emoji":"🏃🏼‍♂️","version":4,"tone":2},{"emoji":"🏃🏽‍♂️","version":4,"tone":3},{"emoji":"🏃🏾‍♂️","version":4,"tone":4},{"emoji":"🏃🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_running"],"annotation":"woman running","tags":["marathon","racing","running","woman"],"emoji":"🏃‍♀️","order":2338,"group":1,"version":4,"skins":[{"emoji":"🏃🏻‍♀️","version":4,"tone":1},{"emoji":"🏃🏼‍♀️","version":4,"tone":2},{"emoji":"🏃🏽‍♀️","version":4,"tone":3},{"emoji":"🏃🏾‍♀️","version":4,"tone":4},{"emoji":"🏃🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["person_running_right"],"annotation":"person running facing right","tags":["arrow","cardinal","direction","east","marathon","person running","right arrow","running"],"emoji":"🏃‍➡️","order":2350,"group":1,"version":15.1,"skins":[{"emoji":"🏃🏻‍➡️","version":15.1,"tone":1},{"emoji":"🏃🏼‍➡️","version":15.1,"tone":2},{"emoji":"🏃🏽‍➡️","version":15.1,"tone":3},{"emoji":"🏃🏾‍➡️","version":15.1,"tone":4},{"emoji":"🏃🏿‍➡️","version":15.1,"tone":5}]},{"shortcodes":["woman_running_right"],"annotation":"woman running facing right","tags":["arrow","cardinal","direction","east","female sign","marathon","person running","right arrow","running","woman"],"emoji":"🏃‍♀️‍➡️","order":2362,"group":1,"version":15.1,"skins":[{"emoji":"🏃🏻‍♀️‍➡️","version":15.1,"tone":1},{"emoji":"🏃🏼‍♀️‍➡️","version":15.1,"tone":2},{"emoji":"🏃🏽‍♀️‍➡️","version":15.1,"tone":3},{"emoji":"🏃🏾‍♀️‍➡️","version":15.1,"tone":4},{"emoji":"🏃🏿‍♀️‍➡️","version":15.1,"tone":5}]},{"shortcodes":["man_running_right"],"annotation":"man running facing right","tags":["arrow","cardinal","direction","east","male sign","man","marathon","person running","right arrow","running"],"emoji":"🏃‍♂️‍➡️","order":2386,"group":1,"version":15.1,"skins":[{"emoji":"🏃🏻‍♂️‍➡️","version":15.1,"tone":1},{"emoji":"🏃🏼‍♂️‍➡️","version":15.1,"tone":2},{"emoji":"🏃🏽‍♂️‍➡️","version":15.1,"tone":3},{"emoji":"🏃🏾‍♂️‍➡️","version":15.1,"tone":4},{"emoji":"🏃🏿‍♂️‍➡️","version":15.1,"tone":5}]},{"shortcodes":["dancer","woman_dancing"],"annotation":"woman dancing","tags":["dance","dancing","woman"],"emoji":"💃","order":2410,"group":1,"version":0.6,"skins":[{"emoji":"💃🏻","version":1,"tone":1},{"emoji":"💃🏼","version":1,"tone":2},{"emoji":"💃🏽","version":1,"tone":3},{"emoji":"💃🏾","version":1,"tone":4},{"emoji":"💃🏿","version":1,"tone":5}]},{"shortcodes":["man_dancing"],"annotation":"man dancing","tags":["dance","dancing","man"],"emoji":"🕺","order":2416,"group":1,"version":3,"skins":[{"emoji":"🕺🏻","version":3,"tone":1},{"emoji":"🕺🏼","version":3,"tone":2},{"emoji":"🕺🏽","version":3,"tone":3},{"emoji":"🕺🏾","version":3,"tone":4},{"emoji":"🕺🏿","version":3,"tone":5}]},{"shortcodes":["levitate","levitating","person_in_suit_levitating"],"annotation":"person in suit levitating","tags":["business","person","suit"],"emoji":"🕴️","order":2423,"group":1,"version":0.7,"skins":[{"emoji":"🕴🏻","version":4,"tone":1},{"emoji":"🕴🏼","version":4,"tone":2},{"emoji":"🕴🏽","version":4,"tone":3},{"emoji":"🕴🏾","version":4,"tone":4},{"emoji":"🕴🏿","version":4,"tone":5}]},{"shortcodes":["dancers","people_with_bunny_ears_partying"],"annotation":"people with bunny ears","tags":["bunny ear","dancer","partying"],"emoji":"👯","order":2429,"group":1,"version":0.6},{"shortcodes":["men_with_bunny_ears_partying"],"annotation":"men with bunny ears","tags":["bunny ear","dancer","men","partying"],"emoji":"👯‍♂️","order":2430,"group":1,"version":4},{"shortcodes":["women_with_bunny_ears_partying"],"annotation":"women with bunny ears","tags":["bunny ear","dancer","partying","women"],"emoji":"👯‍♀️","order":2432,"group":1,"version":4},{"shortcodes":["person_in_steamy_room"],"annotation":"person in steamy room","tags":["sauna","steam room"],"emoji":"🧖","order":2434,"group":1,"version":5,"skins":[{"emoji":"🧖🏻","version":5,"tone":1},{"emoji":"🧖🏼","version":5,"tone":2},{"emoji":"🧖🏽","version":5,"tone":3},{"emoji":"🧖🏾","version":5,"tone":4},{"emoji":"🧖🏿","version":5,"tone":5}]},{"shortcodes":["man_in_steamy_room"],"annotation":"man in steamy room","tags":["sauna","steam room"],"emoji":"🧖‍♂️","order":2440,"group":1,"version":5,"skins":[{"emoji":"🧖🏻‍♂️","version":5,"tone":1},{"emoji":"🧖🏼‍♂️","version":5,"tone":2},{"emoji":"🧖🏽‍♂️","version":5,"tone":3},{"emoji":"🧖🏾‍♂️","version":5,"tone":4},{"emoji":"🧖🏿‍♂️","version":5,"tone":5}]},{"shortcodes":["woman_in_steamy_room"],"annotation":"woman in steamy room","tags":["sauna","steam room"],"emoji":"🧖‍♀️","order":2452,"group":1,"version":5,"skins":[{"emoji":"🧖🏻‍♀️","version":5,"tone":1},{"emoji":"🧖🏼‍♀️","version":5,"tone":2},{"emoji":"🧖🏽‍♀️","version":5,"tone":3},{"emoji":"🧖🏾‍♀️","version":5,"tone":4},{"emoji":"🧖🏿‍♀️","version":5,"tone":5}]},{"shortcodes":["climbing","person_climbing"],"annotation":"person climbing","tags":["climber"],"emoji":"🧗","order":2464,"group":1,"version":5,"skins":[{"emoji":"🧗🏻","version":5,"tone":1},{"emoji":"🧗🏼","version":5,"tone":2},{"emoji":"🧗🏽","version":5,"tone":3},{"emoji":"🧗🏾","version":5,"tone":4},{"emoji":"🧗🏿","version":5,"tone":5}]},{"shortcodes":["man_climbing"],"annotation":"man climbing","tags":["climber"],"emoji":"🧗‍♂️","order":2470,"group":1,"version":5,"skins":[{"emoji":"🧗🏻‍♂️","version":5,"tone":1},{"emoji":"🧗🏼‍♂️","version":5,"tone":2},{"emoji":"🧗🏽‍♂️","version":5,"tone":3},{"emoji":"🧗🏾‍♂️","version":5,"tone":4},{"emoji":"🧗🏿‍♂️","version":5,"tone":5}]},{"shortcodes":["woman_climbing"],"annotation":"woman climbing","tags":["climber"],"emoji":"🧗‍♀️","order":2482,"group":1,"version":5,"skins":[{"emoji":"🧗🏻‍♀️","version":5,"tone":1},{"emoji":"🧗🏼‍♀️","version":5,"tone":2},{"emoji":"🧗🏽‍♀️","version":5,"tone":3},{"emoji":"🧗🏾‍♀️","version":5,"tone":4},{"emoji":"🧗🏿‍♀️","version":5,"tone":5}]},{"shortcodes":["fencer","fencing","person_fencing"],"annotation":"person fencing","tags":["fencer","fencing","sword"],"emoji":"🤺","order":2494,"group":1,"version":3},{"shortcodes":["horse_racing"],"annotation":"horse racing","tags":["horse","jockey","racehorse","racing"],"emoji":"🏇","order":2495,"group":1,"version":1,"skins":[{"emoji":"🏇🏻","version":1,"tone":1},{"emoji":"🏇🏼","version":1,"tone":2},{"emoji":"🏇🏽","version":1,"tone":3},{"emoji":"🏇🏾","version":1,"tone":4},{"emoji":"🏇🏿","version":1,"tone":5}]},{"shortcodes":["person_skiing","skier","skiing"],"annotation":"skier","tags":["ski","snow"],"emoji":"⛷️","order":2502,"group":1,"version":0.7},{"shortcodes":["person_snowboarding","snowboarder","snowboarding"],"annotation":"snowboarder","tags":["ski","snow","snowboard"],"emoji":"🏂️","order":2503,"group":1,"version":0.6,"skins":[{"emoji":"🏂🏻","version":1,"tone":1},{"emoji":"🏂🏼","version":1,"tone":2},{"emoji":"🏂🏽","version":1,"tone":3},{"emoji":"🏂🏾","version":1,"tone":4},{"emoji":"🏂🏿","version":1,"tone":5}]},{"shortcodes":["golfer","golfing","person_golfing"],"annotation":"person golfing","tags":["ball","golf"],"emoji":"🏌️","order":2510,"group":1,"version":0.7,"skins":[{"emoji":"🏌🏻","version":4,"tone":1},{"emoji":"🏌🏼","version":4,"tone":2},{"emoji":"🏌🏽","version":4,"tone":3},{"emoji":"🏌🏾","version":4,"tone":4},{"emoji":"🏌🏿","version":4,"tone":5}]},{"shortcodes":["man_golfing"],"annotation":"man golfing","tags":["golf","man"],"emoji":"🏌️‍♂️","order":2516,"group":1,"version":4,"skins":[{"emoji":"🏌🏻‍♂️","version":4,"tone":1},{"emoji":"🏌🏼‍♂️","version":4,"tone":2},{"emoji":"🏌🏽‍♂️","version":4,"tone":3},{"emoji":"🏌🏾‍♂️","version":4,"tone":4},{"emoji":"🏌🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_golfing"],"annotation":"woman golfing","tags":["golf","woman"],"emoji":"🏌️‍♀️","order":2530,"group":1,"version":4,"skins":[{"emoji":"🏌🏻‍♀️","version":4,"tone":1},{"emoji":"🏌🏼‍♀️","version":4,"tone":2},{"emoji":"🏌🏽‍♀️","version":4,"tone":3},{"emoji":"🏌🏾‍♀️","version":4,"tone":4},{"emoji":"🏌🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["person_surfing","surfer","surfing"],"annotation":"person surfing","tags":["surfing"],"emoji":"🏄️","order":2544,"group":1,"version":0.6,"skins":[{"emoji":"🏄🏻","version":1,"tone":1},{"emoji":"🏄🏼","version":1,"tone":2},{"emoji":"🏄🏽","version":1,"tone":3},{"emoji":"🏄🏾","version":1,"tone":4},{"emoji":"🏄🏿","version":1,"tone":5}]},{"shortcodes":["man_surfing"],"annotation":"man surfing","tags":["man","surfing"],"emoji":"🏄‍♂️","order":2550,"group":1,"version":4,"skins":[{"emoji":"🏄🏻‍♂️","version":4,"tone":1},{"emoji":"🏄🏼‍♂️","version":4,"tone":2},{"emoji":"🏄🏽‍♂️","version":4,"tone":3},{"emoji":"🏄🏾‍♂️","version":4,"tone":4},{"emoji":"🏄🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_surfing"],"annotation":"woman surfing","tags":["surfing","woman"],"emoji":"🏄‍♀️","order":2562,"group":1,"version":4,"skins":[{"emoji":"🏄🏻‍♀️","version":4,"tone":1},{"emoji":"🏄🏼‍♀️","version":4,"tone":2},{"emoji":"🏄🏽‍♀️","version":4,"tone":3},{"emoji":"🏄🏾‍♀️","version":4,"tone":4},{"emoji":"🏄🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["person_rowing_boat","rowboat"],"annotation":"person rowing boat","tags":["boat","rowboat"],"emoji":"🚣","order":2574,"group":1,"version":1,"skins":[{"emoji":"🚣🏻","version":1,"tone":1},{"emoji":"🚣🏼","version":1,"tone":2},{"emoji":"🚣🏽","version":1,"tone":3},{"emoji":"🚣🏾","version":1,"tone":4},{"emoji":"🚣🏿","version":1,"tone":5}]},{"shortcodes":["man_rowing_boat"],"annotation":"man rowing boat","tags":["boat","man","rowboat"],"emoji":"🚣‍♂️","order":2580,"group":1,"version":4,"skins":[{"emoji":"🚣🏻‍♂️","version":4,"tone":1},{"emoji":"🚣🏼‍♂️","version":4,"tone":2},{"emoji":"🚣🏽‍♂️","version":4,"tone":3},{"emoji":"🚣🏾‍♂️","version":4,"tone":4},{"emoji":"🚣🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_rowing_boat"],"annotation":"woman rowing boat","tags":["boat","rowboat","woman"],"emoji":"🚣‍♀️","order":2592,"group":1,"version":4,"skins":[{"emoji":"🚣🏻‍♀️","version":4,"tone":1},{"emoji":"🚣🏼‍♀️","version":4,"tone":2},{"emoji":"🚣🏽‍♀️","version":4,"tone":3},{"emoji":"🚣🏾‍♀️","version":4,"tone":4},{"emoji":"🚣🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["person_swimming","swimmer","swimming"],"annotation":"person swimming","tags":["swim"],"emoji":"🏊️","order":2604,"group":1,"version":0.6,"skins":[{"emoji":"🏊🏻","version":1,"tone":1},{"emoji":"🏊🏼","version":1,"tone":2},{"emoji":"🏊🏽","version":1,"tone":3},{"emoji":"🏊🏾","version":1,"tone":4},{"emoji":"🏊🏿","version":1,"tone":5}]},{"shortcodes":["man_swimming"],"annotation":"man swimming","tags":["man","swim"],"emoji":"🏊‍♂️","order":2610,"group":1,"version":4,"skins":[{"emoji":"🏊🏻‍♂️","version":4,"tone":1},{"emoji":"🏊🏼‍♂️","version":4,"tone":2},{"emoji":"🏊🏽‍♂️","version":4,"tone":3},{"emoji":"🏊🏾‍♂️","version":4,"tone":4},{"emoji":"🏊🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_swimming"],"annotation":"woman swimming","tags":["swim","woman"],"emoji":"🏊‍♀️","order":2622,"group":1,"version":4,"skins":[{"emoji":"🏊🏻‍♀️","version":4,"tone":1},{"emoji":"🏊🏼‍♀️","version":4,"tone":2},{"emoji":"🏊🏽‍♀️","version":4,"tone":3},{"emoji":"🏊🏾‍♀️","version":4,"tone":4},{"emoji":"🏊🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["person_bouncing_ball"],"annotation":"person bouncing ball","tags":["ball"],"emoji":"⛹️","order":2635,"group":1,"version":0.7,"skins":[{"emoji":"⛹🏻","version":2,"tone":1},{"emoji":"⛹🏼","version":2,"tone":2},{"emoji":"⛹🏽","version":2,"tone":3},{"emoji":"⛹🏾","version":2,"tone":4},{"emoji":"⛹🏿","version":2,"tone":5}]},{"shortcodes":["man_bouncing_ball"],"annotation":"man bouncing ball","tags":["ball","man"],"emoji":"⛹️‍♂️","order":2641,"group":1,"version":4,"skins":[{"emoji":"⛹🏻‍♂️","version":4,"tone":1},{"emoji":"⛹🏼‍♂️","version":4,"tone":2},{"emoji":"⛹🏽‍♂️","version":4,"tone":3},{"emoji":"⛹🏾‍♂️","version":4,"tone":4},{"emoji":"⛹🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_bouncing_ball"],"annotation":"woman bouncing ball","tags":["ball","woman"],"emoji":"⛹️‍♀️","order":2655,"group":1,"version":4,"skins":[{"emoji":"⛹🏻‍♀️","version":4,"tone":1},{"emoji":"⛹🏼‍♀️","version":4,"tone":2},{"emoji":"⛹🏽‍♀️","version":4,"tone":3},{"emoji":"⛹🏾‍♀️","version":4,"tone":4},{"emoji":"⛹🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["person_lifting_weights","weight_lifter","weight_lifting"],"annotation":"person lifting weights","tags":["lifter","weight"],"emoji":"🏋️","order":2670,"group":1,"version":0.7,"skins":[{"emoji":"🏋🏻","version":2,"tone":1},{"emoji":"🏋🏼","version":2,"tone":2},{"emoji":"🏋🏽","version":2,"tone":3},{"emoji":"🏋🏾","version":2,"tone":4},{"emoji":"🏋🏿","version":2,"tone":5}]},{"shortcodes":["man_lifting_weights"],"annotation":"man lifting weights","tags":["man","weight lifter"],"emoji":"🏋️‍♂️","order":2676,"group":1,"version":4,"skins":[{"emoji":"🏋🏻‍♂️","version":4,"tone":1},{"emoji":"🏋🏼‍♂️","version":4,"tone":2},{"emoji":"🏋🏽‍♂️","version":4,"tone":3},{"emoji":"🏋🏾‍♂️","version":4,"tone":4},{"emoji":"🏋🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_lifting_weights"],"annotation":"woman lifting weights","tags":["weight lifter","woman"],"emoji":"🏋️‍♀️","order":2690,"group":1,"version":4,"skins":[{"emoji":"🏋🏻‍♀️","version":4,"tone":1},{"emoji":"🏋🏼‍♀️","version":4,"tone":2},{"emoji":"🏋🏽‍♀️","version":4,"tone":3},{"emoji":"🏋🏾‍♀️","version":4,"tone":4},{"emoji":"🏋🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["bicyclist","biking","person_biking"],"annotation":"person biking","tags":["bicycle","biking","cyclist"],"emoji":"🚴","order":2704,"group":1,"version":1,"skins":[{"emoji":"🚴🏻","version":1,"tone":1},{"emoji":"🚴🏼","version":1,"tone":2},{"emoji":"🚴🏽","version":1,"tone":3},{"emoji":"🚴🏾","version":1,"tone":4},{"emoji":"🚴🏿","version":1,"tone":5}]},{"shortcodes":["man_biking"],"annotation":"man biking","tags":["bicycle","biking","cyclist","man"],"emoji":"🚴‍♂️","order":2710,"group":1,"version":4,"skins":[{"emoji":"🚴🏻‍♂️","version":4,"tone":1},{"emoji":"🚴🏼‍♂️","version":4,"tone":2},{"emoji":"🚴🏽‍♂️","version":4,"tone":3},{"emoji":"🚴🏾‍♂️","version":4,"tone":4},{"emoji":"🚴🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_biking"],"annotation":"woman biking","tags":["bicycle","biking","cyclist","woman"],"emoji":"🚴‍♀️","order":2722,"group":1,"version":4,"skins":[{"emoji":"🚴🏻‍♀️","version":4,"tone":1},{"emoji":"🚴🏼‍♀️","version":4,"tone":2},{"emoji":"🚴🏽‍♀️","version":4,"tone":3},{"emoji":"🚴🏾‍♀️","version":4,"tone":4},{"emoji":"🚴🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["mountain_bicyclist","mountain_biking","person_mountain_biking"],"annotation":"person mountain biking","tags":["bicycle","bicyclist","bike","cyclist","mountain"],"emoji":"🚵","order":2734,"group":1,"version":1,"skins":[{"emoji":"🚵🏻","version":1,"tone":1},{"emoji":"🚵🏼","version":1,"tone":2},{"emoji":"🚵🏽","version":1,"tone":3},{"emoji":"🚵🏾","version":1,"tone":4},{"emoji":"🚵🏿","version":1,"tone":5}]},{"shortcodes":["man_mountain_biking"],"annotation":"man mountain biking","tags":["bicycle","bike","cyclist","man","mountain"],"emoji":"🚵‍♂️","order":2740,"group":1,"version":4,"skins":[{"emoji":"🚵🏻‍♂️","version":4,"tone":1},{"emoji":"🚵🏼‍♂️","version":4,"tone":2},{"emoji":"🚵🏽‍♂️","version":4,"tone":3},{"emoji":"🚵🏾‍♂️","version":4,"tone":4},{"emoji":"🚵🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_mountain_biking"],"annotation":"woman mountain biking","tags":["bicycle","bike","biking","cyclist","mountain","woman"],"emoji":"🚵‍♀️","order":2752,"group":1,"version":4,"skins":[{"emoji":"🚵🏻‍♀️","version":4,"tone":1},{"emoji":"🚵🏼‍♀️","version":4,"tone":2},{"emoji":"🚵🏽‍♀️","version":4,"tone":3},{"emoji":"🚵🏾‍♀️","version":4,"tone":4},{"emoji":"🚵🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["cartwheeling","person_cartwheel"],"annotation":"person cartwheeling","tags":["cartwheel","gymnastics"],"emoji":"🤸","order":2764,"group":1,"version":3,"skins":[{"emoji":"🤸🏻","version":3,"tone":1},{"emoji":"🤸🏼","version":3,"tone":2},{"emoji":"🤸🏽","version":3,"tone":3},{"emoji":"🤸🏾","version":3,"tone":4},{"emoji":"🤸🏿","version":3,"tone":5}]},{"shortcodes":["man_cartwheeling"],"annotation":"man cartwheeling","tags":["cartwheel","gymnastics","man"],"emoji":"🤸‍♂️","order":2770,"group":1,"version":4,"skins":[{"emoji":"🤸🏻‍♂️","version":4,"tone":1},{"emoji":"🤸🏼‍♂️","version":4,"tone":2},{"emoji":"🤸🏽‍♂️","version":4,"tone":3},{"emoji":"🤸🏾‍♂️","version":4,"tone":4},{"emoji":"🤸🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_cartwheeling"],"annotation":"woman cartwheeling","tags":["cartwheel","gymnastics","woman"],"emoji":"🤸‍♀️","order":2782,"group":1,"version":4,"skins":[{"emoji":"🤸🏻‍♀️","version":4,"tone":1},{"emoji":"🤸🏼‍♀️","version":4,"tone":2},{"emoji":"🤸🏽‍♀️","version":4,"tone":3},{"emoji":"🤸🏾‍♀️","version":4,"tone":4},{"emoji":"🤸🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["people_wrestling","wrestlers","wrestling"],"annotation":"people wrestling","tags":["wrestle","wrestler"],"emoji":"🤼","order":2794,"group":1,"version":3},{"shortcodes":["men_wrestling"],"annotation":"men wrestling","tags":["men","wrestle"],"emoji":"🤼‍♂️","order":2795,"group":1,"version":4},{"shortcodes":["women_wrestling"],"annotation":"women wrestling","tags":["women","wrestle"],"emoji":"🤼‍♀️","order":2797,"group":1,"version":4},{"shortcodes":["person_playing_water_polo","water_polo"],"annotation":"person playing water polo","tags":["polo","water"],"emoji":"🤽","order":2799,"group":1,"version":3,"skins":[{"emoji":"🤽🏻","version":3,"tone":1},{"emoji":"🤽🏼","version":3,"tone":2},{"emoji":"🤽🏽","version":3,"tone":3},{"emoji":"🤽🏾","version":3,"tone":4},{"emoji":"🤽🏿","version":3,"tone":5}]},{"shortcodes":["man_playing_water_polo"],"annotation":"man playing water polo","tags":["man","water polo"],"emoji":"🤽‍♂️","order":2805,"group":1,"version":4,"skins":[{"emoji":"🤽🏻‍♂️","version":4,"tone":1},{"emoji":"🤽🏼‍♂️","version":4,"tone":2},{"emoji":"🤽🏽‍♂️","version":4,"tone":3},{"emoji":"🤽🏾‍♂️","version":4,"tone":4},{"emoji":"🤽🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_playing_water_polo"],"annotation":"woman playing water polo","tags":["water polo","woman"],"emoji":"🤽‍♀️","order":2817,"group":1,"version":4,"skins":[{"emoji":"🤽🏻‍♀️","version":4,"tone":1},{"emoji":"🤽🏼‍♀️","version":4,"tone":2},{"emoji":"🤽🏽‍♀️","version":4,"tone":3},{"emoji":"🤽🏾‍♀️","version":4,"tone":4},{"emoji":"🤽🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["handball","person_playing_handball"],"annotation":"person playing handball","tags":["ball","handball"],"emoji":"🤾","order":2829,"group":1,"version":3,"skins":[{"emoji":"🤾🏻","version":3,"tone":1},{"emoji":"🤾🏼","version":3,"tone":2},{"emoji":"🤾🏽","version":3,"tone":3},{"emoji":"🤾🏾","version":3,"tone":4},{"emoji":"🤾🏿","version":3,"tone":5}]},{"shortcodes":["man_playing_handball"],"annotation":"man playing handball","tags":["handball","man"],"emoji":"🤾‍♂️","order":2835,"group":1,"version":4,"skins":[{"emoji":"🤾🏻‍♂️","version":4,"tone":1},{"emoji":"🤾🏼‍♂️","version":4,"tone":2},{"emoji":"🤾🏽‍♂️","version":4,"tone":3},{"emoji":"🤾🏾‍♂️","version":4,"tone":4},{"emoji":"🤾🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_playing_handball"],"annotation":"woman playing handball","tags":["handball","woman"],"emoji":"🤾‍♀️","order":2847,"group":1,"version":4,"skins":[{"emoji":"🤾🏻‍♀️","version":4,"tone":1},{"emoji":"🤾🏼‍♀️","version":4,"tone":2},{"emoji":"🤾🏽‍♀️","version":4,"tone":3},{"emoji":"🤾🏾‍♀️","version":4,"tone":4},{"emoji":"🤾🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["juggler","juggling","person_juggling"],"annotation":"person juggling","tags":["balance","juggle","multitask","skill"],"emoji":"🤹","order":2859,"group":1,"version":3,"skins":[{"emoji":"🤹🏻","version":3,"tone":1},{"emoji":"🤹🏼","version":3,"tone":2},{"emoji":"🤹🏽","version":3,"tone":3},{"emoji":"🤹🏾","version":3,"tone":4},{"emoji":"🤹🏿","version":3,"tone":5}]},{"shortcodes":["man_juggling"],"annotation":"man juggling","tags":["juggling","man","multitask"],"emoji":"🤹‍♂️","order":2865,"group":1,"version":4,"skins":[{"emoji":"🤹🏻‍♂️","version":4,"tone":1},{"emoji":"🤹🏼‍♂️","version":4,"tone":2},{"emoji":"🤹🏽‍♂️","version":4,"tone":3},{"emoji":"🤹🏾‍♂️","version":4,"tone":4},{"emoji":"🤹🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_juggling"],"annotation":"woman juggling","tags":["juggling","multitask","woman"],"emoji":"🤹‍♀️","order":2877,"group":1,"version":4,"skins":[{"emoji":"🤹🏻‍♀️","version":4,"tone":1},{"emoji":"🤹🏼‍♀️","version":4,"tone":2},{"emoji":"🤹🏽‍♀️","version":4,"tone":3},{"emoji":"🤹🏾‍♀️","version":4,"tone":4},{"emoji":"🤹🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["person_in_lotus_position"],"annotation":"person in lotus position","tags":["meditation","yoga"],"emoji":"🧘","order":2889,"group":1,"version":5,"skins":[{"emoji":"🧘🏻","version":5,"tone":1},{"emoji":"🧘🏼","version":5,"tone":2},{"emoji":"🧘🏽","version":5,"tone":3},{"emoji":"🧘🏾","version":5,"tone":4},{"emoji":"🧘🏿","version":5,"tone":5}]},{"shortcodes":["man_in_lotus_position"],"annotation":"man in lotus position","tags":["meditation","yoga"],"emoji":"🧘‍♂️","order":2895,"group":1,"version":5,"skins":[{"emoji":"🧘🏻‍♂️","version":5,"tone":1},{"emoji":"🧘🏼‍♂️","version":5,"tone":2},{"emoji":"🧘🏽‍♂️","version":5,"tone":3},{"emoji":"🧘🏾‍♂️","version":5,"tone":4},{"emoji":"🧘🏿‍♂️","version":5,"tone":5}]},{"shortcodes":["woman_in_lotus_position"],"annotation":"woman in lotus position","tags":["meditation","yoga"],"emoji":"🧘‍♀️","order":2907,"group":1,"version":5,"skins":[{"emoji":"🧘🏻‍♀️","version":5,"tone":1},{"emoji":"🧘🏼‍♀️","version":5,"tone":2},{"emoji":"🧘🏽‍♀️","version":5,"tone":3},{"emoji":"🧘🏾‍♀️","version":5,"tone":4},{"emoji":"🧘🏿‍♀️","version":5,"tone":5}]},{"shortcodes":["bath","person_taking_bath"],"annotation":"person taking bath","tags":["bath","bathtub"],"emoji":"🛀","order":2919,"group":1,"version":0.6,"skins":[{"emoji":"🛀🏻","version":1,"tone":1},{"emoji":"🛀🏼","version":1,"tone":2},{"emoji":"🛀🏽","version":1,"tone":3},{"emoji":"🛀🏾","version":1,"tone":4},{"emoji":"🛀🏿","version":1,"tone":5}]},{"shortcodes":["person_in_bed","sleeping_accommodation"],"annotation":"person in bed","tags":["good night","hotel","sleep"],"emoji":"🛌","order":2925,"group":1,"version":1,"skins":[{"emoji":"🛌🏻","version":4,"tone":1},{"emoji":"🛌🏼","version":4,"tone":2},{"emoji":"🛌🏽","version":4,"tone":3},{"emoji":"🛌🏾","version":4,"tone":4},{"emoji":"🛌🏿","version":4,"tone":5}]},{"shortcodes":["people_holding_hands"],"annotation":"people holding hands","tags":["couple","hand","hold","holding hands","person"],"emoji":"🧑‍🤝‍🧑","order":2931,"group":1,"version":12,"skins":[{"emoji":"🧑🏻‍🤝‍🧑🏻","version":12,"tone":1},{"emoji":"🧑🏻‍🤝‍🧑🏼","version":12.1,"tone":[1,2]},{"emoji":"🧑🏻‍🤝‍🧑🏽","version":12.1,"tone":[1,3]},{"emoji":"🧑🏻‍🤝‍🧑🏾","version":12.1,"tone":[1,4]},{"emoji":"🧑🏻‍🤝‍🧑🏿","version":12.1,"tone":[1,5]},{"emoji":"🧑🏼‍🤝‍🧑🏻","version":12,"tone":[2,1]},{"emoji":"🧑🏼‍🤝‍🧑🏼","version":12,"tone":2},{"emoji":"🧑🏼‍🤝‍🧑🏽","version":12.1,"tone":[2,3]},{"emoji":"🧑🏼‍🤝‍🧑🏾","version":12.1,"tone":[2,4]},{"emoji":"🧑🏼‍🤝‍🧑🏿","version":12.1,"tone":[2,5]},{"emoji":"🧑🏽‍🤝‍🧑🏻","version":12,"tone":[3,1]},{"emoji":"🧑🏽‍🤝‍🧑🏼","version":12,"tone":[3,2]},{"emoji":"🧑🏽‍🤝‍🧑🏽","version":12,"tone":3},{"emoji":"🧑🏽‍🤝‍🧑🏾","version":12.1,"tone":[3,4]},{"emoji":"🧑🏽‍🤝‍🧑🏿","version":12.1,"tone":[3,5]},{"emoji":"🧑🏾‍🤝‍🧑🏻","version":12,"tone":[4,1]},{"emoji":"🧑🏾‍🤝‍🧑🏼","version":12,"tone":[4,2]},{"emoji":"🧑🏾‍🤝‍🧑🏽","version":12,"tone":[4,3]},{"emoji":"🧑🏾‍🤝‍🧑🏾","version":12,"tone":4},{"emoji":"🧑🏾‍🤝‍🧑🏿","version":12.1,"tone":[4,5]},{"emoji":"🧑🏿‍🤝‍🧑🏻","version":12,"tone":[5,1]},{"emoji":"🧑🏿‍🤝‍🧑🏼","version":12,"tone":[5,2]},{"emoji":"🧑🏿‍🤝‍🧑🏽","version":12,"tone":[5,3]},{"emoji":"🧑🏿‍🤝‍🧑🏾","version":12,"tone":[5,4]},{"emoji":"🧑🏿‍🤝‍🧑🏿","version":12,"tone":5}]},{"shortcodes":["two_women_holding_hands"],"annotation":"women holding hands","tags":["couple","hand","holding hands","women"],"emoji":"👭","order":2957,"group":1,"version":1,"skins":[{"emoji":"👭🏻","version":12,"tone":1},{"emoji":"👭🏼","version":12,"tone":2},{"emoji":"👭🏽","version":12,"tone":3},{"emoji":"👭🏾","version":12,"tone":4},{"emoji":"👭🏿","version":12,"tone":5},{"emoji":"👩🏻‍🤝‍👩🏼","version":12.1,"tone":[1,2]},{"emoji":"👩🏻‍🤝‍👩🏽","version":12.1,"tone":[1,3]},{"emoji":"👩🏻‍🤝‍👩🏾","version":12.1,"tone":[1,4]},{"emoji":"👩🏻‍🤝‍👩🏿","version":12.1,"tone":[1,5]},{"emoji":"👩🏼‍🤝‍👩🏻","version":12,"tone":[2,1]},{"emoji":"👩🏼‍🤝‍👩🏽","version":12.1,"tone":[2,3]},{"emoji":"👩🏼‍🤝‍👩🏾","version":12.1,"tone":[2,4]},{"emoji":"👩🏼‍🤝‍👩🏿","version":12.1,"tone":[2,5]},{"emoji":"👩🏽‍🤝‍👩🏻","version":12,"tone":[3,1]},{"emoji":"👩🏽‍🤝‍👩🏼","version":12,"tone":[3,2]},{"emoji":"👩🏽‍🤝‍👩🏾","version":12.1,"tone":[3,4]},{"emoji":"👩🏽‍🤝‍👩🏿","version":12.1,"tone":[3,5]},{"emoji":"👩🏾‍🤝‍👩🏻","version":12,"tone":[4,1]},{"emoji":"👩🏾‍🤝‍👩🏼","version":12,"tone":[4,2]},{"emoji":"👩🏾‍🤝‍👩🏽","version":12,"tone":[4,3]},{"emoji":"👩🏾‍🤝‍👩🏿","version":12.1,"tone":[4,5]},{"emoji":"👩🏿‍🤝‍👩🏻","version":12,"tone":[5,1]},{"emoji":"👩🏿‍🤝‍👩🏼","version":12,"tone":[5,2]},{"emoji":"👩🏿‍🤝‍👩🏽","version":12,"tone":[5,3]},{"emoji":"👩🏿‍🤝‍👩🏾","version":12,"tone":[5,4]}]},{"shortcodes":["couple"],"annotation":"woman and man holding hands","tags":["couple","hand","hold","holding hands","man","woman"],"emoji":"👫","order":2983,"group":1,"version":0.6,"skins":[{"emoji":"👫🏻","version":12,"tone":1},{"emoji":"👫🏼","version":12,"tone":2},{"emoji":"👫🏽","version":12,"tone":3},{"emoji":"👫🏾","version":12,"tone":4},{"emoji":"👫🏿","version":12,"tone":5},{"emoji":"👩🏻‍🤝‍👨🏼","version":12,"tone":[1,2]},{"emoji":"👩🏻‍🤝‍👨🏽","version":12,"tone":[1,3]},{"emoji":"👩🏻‍🤝‍👨🏾","version":12,"tone":[1,4]},{"emoji":"👩🏻‍🤝‍👨🏿","version":12,"tone":[1,5]},{"emoji":"👩🏼‍🤝‍👨🏻","version":12,"tone":[2,1]},{"emoji":"👩🏼‍🤝‍👨🏽","version":12,"tone":[2,3]},{"emoji":"👩🏼‍🤝‍👨🏾","version":12,"tone":[2,4]},{"emoji":"👩🏼‍🤝‍👨🏿","version":12,"tone":[2,5]},{"emoji":"👩🏽‍🤝‍👨🏻","version":12,"tone":[3,1]},{"emoji":"👩🏽‍🤝‍👨🏼","version":12,"tone":[3,2]},{"emoji":"👩🏽‍🤝‍👨🏾","version":12,"tone":[3,4]},{"emoji":"👩🏽‍🤝‍👨🏿","version":12,"tone":[3,5]},{"emoji":"👩🏾‍🤝‍👨🏻","version":12,"tone":[4,1]},{"emoji":"👩🏾‍🤝‍👨🏼","version":12,"tone":[4,2]},{"emoji":"👩🏾‍🤝‍👨🏽","version":12,"tone":[4,3]},{"emoji":"👩🏾‍🤝‍👨🏿","version":12,"tone":[4,5]},{"emoji":"👩🏿‍🤝‍👨🏻","version":12,"tone":[5,1]},{"emoji":"👩🏿‍🤝‍👨🏼","version":12,"tone":[5,2]},{"emoji":"👩🏿‍🤝‍👨🏽","version":12,"tone":[5,3]},{"emoji":"👩🏿‍🤝‍👨🏾","version":12,"tone":[5,4]}]},{"shortcodes":["two_men_holding_hands"],"annotation":"men holding hands","tags":["couple","gemini","holding hands","man","men","twins","zodiac"],"emoji":"👬","order":3009,"group":1,"version":1,"skins":[{"emoji":"👬🏻","version":12,"tone":1},{"emoji":"👬🏼","version":12,"tone":2},{"emoji":"👬🏽","version":12,"tone":3},{"emoji":"👬🏾","version":12,"tone":4},{"emoji":"👬🏿","version":12,"tone":5},{"emoji":"👨🏻‍🤝‍👨🏼","version":12.1,"tone":[1,2]},{"emoji":"👨🏻‍🤝‍👨🏽","version":12.1,"tone":[1,3]},{"emoji":"👨🏻‍🤝‍👨🏾","version":12.1,"tone":[1,4]},{"emoji":"👨🏻‍🤝‍👨🏿","version":12.1,"tone":[1,5]},{"emoji":"👨🏼‍🤝‍👨🏻","version":12,"tone":[2,1]},{"emoji":"👨🏼‍🤝‍👨🏽","version":12.1,"tone":[2,3]},{"emoji":"👨🏼‍🤝‍👨🏾","version":12.1,"tone":[2,4]},{"emoji":"👨🏼‍🤝‍👨🏿","version":12.1,"tone":[2,5]},{"emoji":"👨🏽‍🤝‍👨🏻","version":12,"tone":[3,1]},{"emoji":"👨🏽‍🤝‍👨🏼","version":12,"tone":[3,2]},{"emoji":"👨🏽‍🤝‍👨🏾","version":12.1,"tone":[3,4]},{"emoji":"👨🏽‍🤝‍👨🏿","version":12.1,"tone":[3,5]},{"emoji":"👨🏾‍🤝‍👨🏻","version":12,"tone":[4,1]},{"emoji":"👨🏾‍🤝‍👨🏼","version":12,"tone":[4,2]},{"emoji":"👨🏾‍🤝‍👨🏽","version":12,"tone":[4,3]},{"emoji":"👨🏾‍🤝‍👨🏿","version":12.1,"tone":[4,5]},{"emoji":"👨🏿‍🤝‍👨🏻","version":12,"tone":[5,1]},{"emoji":"👨🏿‍🤝‍👨🏼","version":12,"tone":[5,2]},{"emoji":"👨🏿‍🤝‍👨🏽","version":12,"tone":[5,3]},{"emoji":"👨🏿‍🤝‍👨🏾","version":12,"tone":[5,4]}]},{"shortcodes":["couple_kiss","couplekiss"],"annotation":"kiss","tags":["couple"],"emoji":"💏","order":3035,"group":1,"version":0.6,"skins":[{"emoji":"💏🏻","version":13.1,"tone":1},{"emoji":"💏🏼","version":13.1,"tone":2},{"emoji":"💏🏽","version":13.1,"tone":3},{"emoji":"💏🏾","version":13.1,"tone":4},{"emoji":"💏🏿","version":13.1,"tone":5},{"emoji":"🧑🏻‍❤️‍💋‍🧑🏼","version":13.1,"tone":[1,2]},{"emoji":"🧑🏻‍❤️‍💋‍🧑🏽","version":13.1,"tone":[1,3]},{"emoji":"🧑🏻‍❤️‍💋‍🧑🏾","version":13.1,"tone":[1,4]},{"emoji":"🧑🏻‍❤️‍💋‍🧑🏿","version":13.1,"tone":[1,5]},{"emoji":"🧑🏼‍❤️‍💋‍🧑🏻","version":13.1,"tone":[2,1]},{"emoji":"🧑🏼‍❤️‍💋‍🧑🏽","version":13.1,"tone":[2,3]},{"emoji":"🧑🏼‍❤️‍💋‍🧑🏾","version":13.1,"tone":[2,4]},{"emoji":"🧑🏼‍❤️‍💋‍🧑🏿","version":13.1,"tone":[2,5]},{"emoji":"🧑🏽‍❤️‍💋‍🧑🏻","version":13.1,"tone":[3,1]},{"emoji":"🧑🏽‍❤️‍💋‍🧑🏼","version":13.1,"tone":[3,2]},{"emoji":"🧑🏽‍❤️‍💋‍🧑🏾","version":13.1,"tone":[3,4]},{"emoji":"🧑🏽‍❤️‍💋‍🧑🏿","version":13.1,"tone":[3,5]},{"emoji":"🧑🏾‍❤️‍💋‍🧑🏻","version":13.1,"tone":[4,1]},{"emoji":"🧑🏾‍❤️‍💋‍🧑🏼","version":13.1,"tone":[4,2]},{"emoji":"🧑🏾‍❤️‍💋‍🧑🏽","version":13.1,"tone":[4,3]},{"emoji":"🧑🏾‍❤️‍💋‍🧑🏿","version":13.1,"tone":[4,5]},{"emoji":"🧑🏿‍❤️‍💋‍🧑🏻","version":13.1,"tone":[5,1]},{"emoji":"🧑🏿‍❤️‍💋‍🧑🏼","version":13.1,"tone":[5,2]},{"emoji":"🧑🏿‍❤️‍💋‍🧑🏽","version":13.1,"tone":[5,3]},{"emoji":"🧑🏿‍❤️‍💋‍🧑🏾","version":13.1,"tone":[5,4]}]},{"shortcodes":["kiss_mw","kiss_wm"],"annotation":"kiss: woman, man","tags":["couple","kiss","man","woman"],"emoji":"👩‍❤️‍💋‍👨","order":3081,"group":1,"version":2,"skins":[{"emoji":"👩🏻‍❤️‍💋‍👨🏻","version":13.1,"tone":1},{"emoji":"👩🏻‍❤️‍💋‍👨🏼","version":13.1,"tone":[1,2]},{"emoji":"👩🏻‍❤️‍💋‍👨🏽","version":13.1,"tone":[1,3]},{"emoji":"👩🏻‍❤️‍💋‍👨🏾","version":13.1,"tone":[1,4]},{"emoji":"👩🏻‍❤️‍💋‍👨🏿","version":13.1,"tone":[1,5]},{"emoji":"👩🏼‍❤️‍💋‍👨🏻","version":13.1,"tone":[2,1]},{"emoji":"👩🏼‍❤️‍💋‍👨🏼","version":13.1,"tone":2},{"emoji":"👩🏼‍❤️‍💋‍👨🏽","version":13.1,"tone":[2,3]},{"emoji":"👩🏼‍❤️‍💋‍👨🏾","version":13.1,"tone":[2,4]},{"emoji":"👩🏼‍❤️‍💋‍👨🏿","version":13.1,"tone":[2,5]},{"emoji":"👩🏽‍❤️‍💋‍👨🏻","version":13.1,"tone":[3,1]},{"emoji":"👩🏽‍❤️‍💋‍👨🏼","version":13.1,"tone":[3,2]},{"emoji":"👩🏽‍❤️‍💋‍👨🏽","version":13.1,"tone":3},{"emoji":"👩🏽‍❤️‍💋‍👨🏾","version":13.1,"tone":[3,4]},{"emoji":"👩🏽‍❤️‍💋‍👨🏿","version":13.1,"tone":[3,5]},{"emoji":"👩🏾‍❤️‍💋‍👨🏻","version":13.1,"tone":[4,1]},{"emoji":"👩🏾‍❤️‍💋‍👨🏼","version":13.1,"tone":[4,2]},{"emoji":"👩🏾‍❤️‍💋‍👨🏽","version":13.1,"tone":[4,3]},{"emoji":"👩🏾‍❤️‍💋‍👨🏾","version":13.1,"tone":4},{"emoji":"👩🏾‍❤️‍💋‍👨🏿","version":13.1,"tone":[4,5]},{"emoji":"👩🏿‍❤️‍💋‍👨🏻","version":13.1,"tone":[5,1]},{"emoji":"👩🏿‍❤️‍💋‍👨🏼","version":13.1,"tone":[5,2]},{"emoji":"👩🏿‍❤️‍💋‍👨🏽","version":13.1,"tone":[5,3]},{"emoji":"👩🏿‍❤️‍💋‍👨🏾","version":13.1,"tone":[5,4]},{"emoji":"👩🏿‍❤️‍💋‍👨🏿","version":13.1,"tone":5}]},{"shortcodes":["kiss_mm"],"annotation":"kiss: man, man","tags":["couple","kiss","man"],"emoji":"👨‍❤️‍💋‍👨","order":3133,"group":1,"version":2,"skins":[{"emoji":"👨🏻‍❤️‍💋‍👨🏻","version":13.1,"tone":1},{"emoji":"👨🏻‍❤️‍💋‍👨🏼","version":13.1,"tone":[1,2]},{"emoji":"👨🏻‍❤️‍💋‍👨🏽","version":13.1,"tone":[1,3]},{"emoji":"👨🏻‍❤️‍💋‍👨🏾","version":13.1,"tone":[1,4]},{"emoji":"👨🏻‍❤️‍💋‍👨🏿","version":13.1,"tone":[1,5]},{"emoji":"👨🏼‍❤️‍💋‍👨🏻","version":13.1,"tone":[2,1]},{"emoji":"👨🏼‍❤️‍💋‍👨🏼","version":13.1,"tone":2},{"emoji":"👨🏼‍❤️‍💋‍👨🏽","version":13.1,"tone":[2,3]},{"emoji":"👨🏼‍❤️‍💋‍👨🏾","version":13.1,"tone":[2,4]},{"emoji":"👨🏼‍❤️‍💋‍👨🏿","version":13.1,"tone":[2,5]},{"emoji":"👨🏽‍❤️‍💋‍👨🏻","version":13.1,"tone":[3,1]},{"emoji":"👨🏽‍❤️‍💋‍👨🏼","version":13.1,"tone":[3,2]},{"emoji":"👨🏽‍❤️‍💋‍👨🏽","version":13.1,"tone":3},{"emoji":"👨🏽‍❤️‍💋‍👨🏾","version":13.1,"tone":[3,4]},{"emoji":"👨🏽‍❤️‍💋‍👨🏿","version":13.1,"tone":[3,5]},{"emoji":"👨🏾‍❤️‍💋‍👨🏻","version":13.1,"tone":[4,1]},{"emoji":"👨🏾‍❤️‍💋‍👨🏼","version":13.1,"tone":[4,2]},{"emoji":"👨🏾‍❤️‍💋‍👨🏽","version":13.1,"tone":[4,3]},{"emoji":"👨🏾‍❤️‍💋‍👨🏾","version":13.1,"tone":4},{"emoji":"👨🏾‍❤️‍💋‍👨🏿","version":13.1,"tone":[4,5]},{"emoji":"👨🏿‍❤️‍💋‍👨🏻","version":13.1,"tone":[5,1]},{"emoji":"👨🏿‍❤️‍💋‍👨🏼","version":13.1,"tone":[5,2]},{"emoji":"👨🏿‍❤️‍💋‍👨🏽","version":13.1,"tone":[5,3]},{"emoji":"👨🏿‍❤️‍💋‍👨🏾","version":13.1,"tone":[5,4]},{"emoji":"👨🏿‍❤️‍💋‍👨🏿","version":13.1,"tone":5}]},{"shortcodes":["kiss_ww"],"annotation":"kiss: woman, woman","tags":["couple","kiss","woman"],"emoji":"👩‍❤️‍💋‍👩","order":3185,"group":1,"version":2,"skins":[{"emoji":"👩🏻‍❤️‍💋‍👩🏻","version":13.1,"tone":1},{"emoji":"👩🏻‍❤️‍💋‍👩🏼","version":13.1,"tone":[1,2]},{"emoji":"👩🏻‍❤️‍💋‍👩🏽","version":13.1,"tone":[1,3]},{"emoji":"👩🏻‍❤️‍💋‍👩🏾","version":13.1,"tone":[1,4]},{"emoji":"👩🏻‍❤️‍💋‍👩🏿","version":13.1,"tone":[1,5]},{"emoji":"👩🏼‍❤️‍💋‍👩🏻","version":13.1,"tone":[2,1]},{"emoji":"👩🏼‍❤️‍💋‍👩🏼","version":13.1,"tone":2},{"emoji":"👩🏼‍❤️‍💋‍👩🏽","version":13.1,"tone":[2,3]},{"emoji":"👩🏼‍❤️‍💋‍👩🏾","version":13.1,"tone":[2,4]},{"emoji":"👩🏼‍❤️‍💋‍👩🏿","version":13.1,"tone":[2,5]},{"emoji":"👩🏽‍❤️‍💋‍👩🏻","version":13.1,"tone":[3,1]},{"emoji":"👩🏽‍❤️‍💋‍👩🏼","version":13.1,"tone":[3,2]},{"emoji":"👩🏽‍❤️‍💋‍👩🏽","version":13.1,"tone":3},{"emoji":"👩🏽‍❤️‍💋‍👩🏾","version":13.1,"tone":[3,4]},{"emoji":"👩🏽‍❤️‍💋‍👩🏿","version":13.1,"tone":[3,5]},{"emoji":"👩🏾‍❤️‍💋‍👩🏻","version":13.1,"tone":[4,1]},{"emoji":"👩🏾‍❤️‍💋‍👩🏼","version":13.1,"tone":[4,2]},{"emoji":"👩🏾‍❤️‍💋‍👩🏽","version":13.1,"tone":[4,3]},{"emoji":"👩🏾‍❤️‍💋‍👩🏾","version":13.1,"tone":4},{"emoji":"👩🏾‍❤️‍💋‍👩🏿","version":13.1,"tone":[4,5]},{"emoji":"👩🏿‍❤️‍💋‍👩🏻","version":13.1,"tone":[5,1]},{"emoji":"👩🏿‍❤️‍💋‍👩🏼","version":13.1,"tone":[5,2]},{"emoji":"👩🏿‍❤️‍💋‍👩🏽","version":13.1,"tone":[5,3]},{"emoji":"👩🏿‍❤️‍💋‍👩🏾","version":13.1,"tone":[5,4]},{"emoji":"👩🏿‍❤️‍💋‍👩🏿","version":13.1,"tone":5}]},{"shortcodes":["couple_with_heart"],"annotation":"couple with heart","tags":["couple","love"],"emoji":"💑","order":3237,"group":1,"version":0.6,"skins":[{"emoji":"💑🏻","version":13.1,"tone":1},{"emoji":"💑🏼","version":13.1,"tone":2},{"emoji":"💑🏽","version":13.1,"tone":3},{"emoji":"💑🏾","version":13.1,"tone":4},{"emoji":"💑🏿","version":13.1,"tone":5},{"emoji":"🧑🏻‍❤️‍🧑🏼","version":13.1,"tone":[1,2]},{"emoji":"🧑🏻‍❤️‍🧑🏽","version":13.1,"tone":[1,3]},{"emoji":"🧑🏻‍❤️‍🧑🏾","version":13.1,"tone":[1,4]},{"emoji":"🧑🏻‍❤️‍🧑🏿","version":13.1,"tone":[1,5]},{"emoji":"🧑🏼‍❤️‍🧑🏻","version":13.1,"tone":[2,1]},{"emoji":"🧑🏼‍❤️‍🧑🏽","version":13.1,"tone":[2,3]},{"emoji":"🧑🏼‍❤️‍🧑🏾","version":13.1,"tone":[2,4]},{"emoji":"🧑🏼‍❤️‍🧑🏿","version":13.1,"tone":[2,5]},{"emoji":"🧑🏽‍❤️‍🧑🏻","version":13.1,"tone":[3,1]},{"emoji":"🧑🏽‍❤️‍🧑🏼","version":13.1,"tone":[3,2]},{"emoji":"🧑🏽‍❤️‍🧑🏾","version":13.1,"tone":[3,4]},{"emoji":"🧑🏽‍❤️‍🧑🏿","version":13.1,"tone":[3,5]},{"emoji":"🧑🏾‍❤️‍🧑🏻","version":13.1,"tone":[4,1]},{"emoji":"🧑🏾‍❤️‍🧑🏼","version":13.1,"tone":[4,2]},{"emoji":"🧑🏾‍❤️‍🧑🏽","version":13.1,"tone":[4,3]},{"emoji":"🧑🏾‍❤️‍🧑🏿","version":13.1,"tone":[4,5]},{"emoji":"🧑🏿‍❤️‍🧑🏻","version":13.1,"tone":[5,1]},{"emoji":"🧑🏿‍❤️‍🧑🏼","version":13.1,"tone":[5,2]},{"emoji":"🧑🏿‍❤️‍🧑🏽","version":13.1,"tone":[5,3]},{"emoji":"🧑🏿‍❤️‍🧑🏾","version":13.1,"tone":[5,4]}]},{"shortcodes":["couple_with_heart_mw","couple_with_heart_wm"],"annotation":"couple with heart: woman, man","tags":["couple","couple with heart","love","man","woman"],"emoji":"👩‍❤️‍👨","order":3283,"group":1,"version":2,"skins":[{"emoji":"👩🏻‍❤️‍👨🏻","version":13.1,"tone":1},{"emoji":"👩🏻‍❤️‍👨🏼","version":13.1,"tone":[1,2]},{"emoji":"👩🏻‍❤️‍👨🏽","version":13.1,"tone":[1,3]},{"emoji":"👩🏻‍❤️‍👨🏾","version":13.1,"tone":[1,4]},{"emoji":"👩🏻‍❤️‍👨🏿","version":13.1,"tone":[1,5]},{"emoji":"👩🏼‍❤️‍👨🏻","version":13.1,"tone":[2,1]},{"emoji":"👩🏼‍❤️‍👨🏼","version":13.1,"tone":2},{"emoji":"👩🏼‍❤️‍👨🏽","version":13.1,"tone":[2,3]},{"emoji":"👩🏼‍❤️‍👨🏾","version":13.1,"tone":[2,4]},{"emoji":"👩🏼‍❤️‍👨🏿","version":13.1,"tone":[2,5]},{"emoji":"👩🏽‍❤️‍👨🏻","version":13.1,"tone":[3,1]},{"emoji":"👩🏽‍❤️‍👨🏼","version":13.1,"tone":[3,2]},{"emoji":"👩🏽‍❤️‍👨🏽","version":13.1,"tone":3},{"emoji":"👩🏽‍❤️‍👨🏾","version":13.1,"tone":[3,4]},{"emoji":"👩🏽‍❤️‍👨🏿","version":13.1,"tone":[3,5]},{"emoji":"👩🏾‍❤️‍👨🏻","version":13.1,"tone":[4,1]},{"emoji":"👩🏾‍❤️‍👨🏼","version":13.1,"tone":[4,2]},{"emoji":"👩🏾‍❤️‍👨🏽","version":13.1,"tone":[4,3]},{"emoji":"👩🏾‍❤️‍👨🏾","version":13.1,"tone":4},{"emoji":"👩🏾‍❤️‍👨🏿","version":13.1,"tone":[4,5]},{"emoji":"👩🏿‍❤️‍👨🏻","version":13.1,"tone":[5,1]},{"emoji":"👩🏿‍❤️‍👨🏼","version":13.1,"tone":[5,2]},{"emoji":"👩🏿‍❤️‍👨🏽","version":13.1,"tone":[5,3]},{"emoji":"👩🏿‍❤️‍👨🏾","version":13.1,"tone":[5,4]},{"emoji":"👩🏿‍❤️‍👨🏿","version":13.1,"tone":5}]},{"shortcodes":["couple_with_heart_mm"],"annotation":"couple with heart: man, man","tags":["couple","couple with heart","love","man"],"emoji":"👨‍❤️‍👨","order":3335,"group":1,"version":2,"skins":[{"emoji":"👨🏻‍❤️‍👨🏻","version":13.1,"tone":1},{"emoji":"👨🏻‍❤️‍👨🏼","version":13.1,"tone":[1,2]},{"emoji":"👨🏻‍❤️‍👨🏽","version":13.1,"tone":[1,3]},{"emoji":"👨🏻‍❤️‍👨🏾","version":13.1,"tone":[1,4]},{"emoji":"👨🏻‍❤️‍👨🏿","version":13.1,"tone":[1,5]},{"emoji":"👨🏼‍❤️‍👨🏻","version":13.1,"tone":[2,1]},{"emoji":"👨🏼‍❤️‍👨🏼","version":13.1,"tone":2},{"emoji":"👨🏼‍❤️‍👨🏽","version":13.1,"tone":[2,3]},{"emoji":"👨🏼‍❤️‍👨🏾","version":13.1,"tone":[2,4]},{"emoji":"👨🏼‍❤️‍👨🏿","version":13.1,"tone":[2,5]},{"emoji":"👨🏽‍❤️‍👨🏻","version":13.1,"tone":[3,1]},{"emoji":"👨🏽‍❤️‍👨🏼","version":13.1,"tone":[3,2]},{"emoji":"👨🏽‍❤️‍👨🏽","version":13.1,"tone":3},{"emoji":"👨🏽‍❤️‍👨🏾","version":13.1,"tone":[3,4]},{"emoji":"👨🏽‍❤️‍👨🏿","version":13.1,"tone":[3,5]},{"emoji":"👨🏾‍❤️‍👨🏻","version":13.1,"tone":[4,1]},{"emoji":"👨🏾‍❤️‍👨🏼","version":13.1,"tone":[4,2]},{"emoji":"👨🏾‍❤️‍👨🏽","version":13.1,"tone":[4,3]},{"emoji":"👨🏾‍❤️‍👨🏾","version":13.1,"tone":4},{"emoji":"👨🏾‍❤️‍👨🏿","version":13.1,"tone":[4,5]},{"emoji":"👨🏿‍❤️‍👨🏻","version":13.1,"tone":[5,1]},{"emoji":"👨🏿‍❤️‍👨🏼","version":13.1,"tone":[5,2]},{"emoji":"👨🏿‍❤️‍👨🏽","version":13.1,"tone":[5,3]},{"emoji":"👨🏿‍❤️‍👨🏾","version":13.1,"tone":[5,4]},{"emoji":"👨🏿‍❤️‍👨🏿","version":13.1,"tone":5}]},{"shortcodes":["couple_with_heart_ww"],"annotation":"couple with heart: woman, woman","tags":["couple","couple with heart","love","woman"],"emoji":"👩‍❤️‍👩","order":3387,"group":1,"version":2,"skins":[{"emoji":"👩🏻‍❤️‍👩🏻","version":13.1,"tone":1},{"emoji":"👩🏻‍❤️‍👩🏼","version":13.1,"tone":[1,2]},{"emoji":"👩🏻‍❤️‍👩🏽","version":13.1,"tone":[1,3]},{"emoji":"👩🏻‍❤️‍👩🏾","version":13.1,"tone":[1,4]},{"emoji":"👩🏻‍❤️‍👩🏿","version":13.1,"tone":[1,5]},{"emoji":"👩🏼‍❤️‍👩🏻","version":13.1,"tone":[2,1]},{"emoji":"👩🏼‍❤️‍👩🏼","version":13.1,"tone":2},{"emoji":"👩🏼‍❤️‍👩🏽","version":13.1,"tone":[2,3]},{"emoji":"👩🏼‍❤️‍👩🏾","version":13.1,"tone":[2,4]},{"emoji":"👩🏼‍❤️‍👩🏿","version":13.1,"tone":[2,5]},{"emoji":"👩🏽‍❤️‍👩🏻","version":13.1,"tone":[3,1]},{"emoji":"👩🏽‍❤️‍👩🏼","version":13.1,"tone":[3,2]},{"emoji":"👩🏽‍❤️‍👩🏽","version":13.1,"tone":3},{"emoji":"👩🏽‍❤️‍👩🏾","version":13.1,"tone":[3,4]},{"emoji":"👩🏽‍❤️‍👩🏿","version":13.1,"tone":[3,5]},{"emoji":"👩🏾‍❤️‍👩🏻","version":13.1,"tone":[4,1]},{"emoji":"👩🏾‍❤️‍👩🏼","version":13.1,"tone":[4,2]},{"emoji":"👩🏾‍❤️‍👩🏽","version":13.1,"tone":[4,3]},{"emoji":"👩🏾‍❤️‍👩🏾","version":13.1,"tone":4},{"emoji":"👩🏾‍❤️‍👩🏿","version":13.1,"tone":[4,5]},{"emoji":"👩🏿‍❤️‍👩🏻","version":13.1,"tone":[5,1]},{"emoji":"👩🏿‍❤️‍👩🏼","version":13.1,"tone":[5,2]},{"emoji":"👩🏿‍❤️‍👩🏽","version":13.1,"tone":[5,3]},{"emoji":"👩🏿‍❤️‍👩🏾","version":13.1,"tone":[5,4]},{"emoji":"👩🏿‍❤️‍👩🏿","version":13.1,"tone":5}]},{"shortcodes":["family_mwb"],"annotation":"family: man, woman, boy","tags":["boy","family","man","woman"],"emoji":"👨‍👩‍👦","order":3439,"group":1,"version":2},{"shortcodes":["family_mwg"],"annotation":"family: man, woman, girl","tags":["family","girl","man","woman"],"emoji":"👨‍👩‍👧","order":3440,"group":1,"version":2},{"shortcodes":["family_mwgb"],"annotation":"family: man, woman, girl, boy","tags":["boy","family","girl","man","woman"],"emoji":"👨‍👩‍👧‍👦","order":3441,"group":1,"version":2},{"shortcodes":["family_mwbb"],"annotation":"family: man, woman, boy, boy","tags":["boy","family","man","woman"],"emoji":"👨‍👩‍👦‍👦","order":3442,"group":1,"version":2},{"shortcodes":["family_mwgg"],"annotation":"family: man, woman, girl, girl","tags":["family","girl","man","woman"],"emoji":"👨‍👩‍👧‍👧","order":3443,"group":1,"version":2},{"shortcodes":["family_mmb"],"annotation":"family: man, man, boy","tags":["boy","family","man"],"emoji":"👨‍👨‍👦","order":3444,"group":1,"version":2},{"shortcodes":["family_mmg"],"annotation":"family: man, man, girl","tags":["family","girl","man"],"emoji":"👨‍👨‍👧","order":3445,"group":1,"version":2},{"shortcodes":["family_mmgb"],"annotation":"family: man, man, girl, boy","tags":["boy","family","girl","man"],"emoji":"👨‍👨‍👧‍👦","order":3446,"group":1,"version":2},{"shortcodes":["family_mmbb"],"annotation":"family: man, man, boy, boy","tags":["boy","family","man"],"emoji":"👨‍👨‍👦‍👦","order":3447,"group":1,"version":2},{"shortcodes":["family_mmgg"],"annotation":"family: man, man, girl, girl","tags":["family","girl","man"],"emoji":"👨‍👨‍👧‍👧","order":3448,"group":1,"version":2},{"shortcodes":["family_wwb"],"annotation":"family: woman, woman, boy","tags":["boy","family","woman"],"emoji":"👩‍👩‍👦","order":3449,"group":1,"version":2},{"shortcodes":["family_wwg"],"annotation":"family: woman, woman, girl","tags":["family","girl","woman"],"emoji":"👩‍👩‍👧","order":3450,"group":1,"version":2},{"shortcodes":["family_wwgb"],"annotation":"family: woman, woman, girl, boy","tags":["boy","family","girl","woman"],"emoji":"👩‍👩‍👧‍👦","order":3451,"group":1,"version":2},{"shortcodes":["family_wwbb"],"annotation":"family: woman, woman, boy, boy","tags":["boy","family","woman"],"emoji":"👩‍👩‍👦‍👦","order":3452,"group":1,"version":2},{"shortcodes":["family_wwgg"],"annotation":"family: woman, woman, girl, girl","tags":["family","girl","woman"],"emoji":"👩‍👩‍👧‍👧","order":3453,"group":1,"version":2},{"shortcodes":["family_mb"],"annotation":"family: man, boy","tags":["boy","family","man"],"emoji":"👨‍👦","order":3454,"group":1,"version":4},{"shortcodes":["family_mbb"],"annotation":"family: man, boy, boy","tags":["boy","family","man"],"emoji":"👨‍👦‍👦","order":3455,"group":1,"version":4},{"shortcodes":["family_mg"],"annotation":"family: man, girl","tags":["family","girl","man"],"emoji":"👨‍👧","order":3456,"group":1,"version":4},{"shortcodes":["family_mgb"],"annotation":"family: man, girl, boy","tags":["boy","family","girl","man"],"emoji":"👨‍👧‍👦","order":3457,"group":1,"version":4},{"shortcodes":["family_mgg"],"annotation":"family: man, girl, girl","tags":["family","girl","man"],"emoji":"👨‍👧‍👧","order":3458,"group":1,"version":4},{"shortcodes":["family_wb"],"annotation":"family: woman, boy","tags":["boy","family","woman"],"emoji":"👩‍👦","order":3459,"group":1,"version":4},{"shortcodes":["family_wbb"],"annotation":"family: woman, boy, boy","tags":["boy","family","woman"],"emoji":"👩‍👦‍👦","order":3460,"group":1,"version":4},{"shortcodes":["family_wg"],"annotation":"family: woman, girl","tags":["family","girl","woman"],"emoji":"👩‍👧","order":3461,"group":1,"version":4},{"shortcodes":["family_wgb"],"annotation":"family: woman, girl, boy","tags":["boy","family","girl","woman"],"emoji":"👩‍👧‍👦","order":3462,"group":1,"version":4},{"shortcodes":["family_wgg"],"annotation":"family: woman, girl, girl","tags":["family","girl","woman"],"emoji":"👩‍👧‍👧","order":3463,"group":1,"version":4},{"shortcodes":["speaking_head"],"annotation":"speaking head","tags":["face","head","silhouette","speak","speaking"],"emoji":"🗣️","order":3465,"group":1,"version":0.7},{"shortcodes":["bust_in_silhouette"],"annotation":"bust in silhouette","tags":["bust","silhouette"],"emoji":"👤","order":3466,"group":1,"version":0.6},{"shortcodes":["busts_in_silhouette"],"annotation":"busts in silhouette","tags":["bust","silhouette"],"emoji":"👥","order":3467,"group":1,"version":1},{"shortcodes":["people_hugging"],"annotation":"people hugging","tags":["goodbye","hello","hug","thanks"],"emoji":"🫂","order":3468,"group":1,"version":13},{"shortcodes":["family"],"annotation":"family","tags":["family"],"emoji":"👪️","order":3469,"group":1,"version":0.6},{"shortcodes":["family_aac"],"annotation":"family: adult, adult, child","tags":["family: adult, adult, child"],"emoji":"🧑‍🧑‍🧒","order":3470,"group":1,"version":15.1},{"shortcodes":["family_aacc"],"annotation":"family: adult, adult, child, child","tags":["family: adult, adult, child, child"],"emoji":"🧑‍🧑‍🧒‍🧒","order":3471,"group":1,"version":15.1},{"shortcodes":["family_ac"],"annotation":"family: adult, child","tags":["family: adult, child"],"emoji":"🧑‍🧒","order":3472,"group":1,"version":15.1},{"shortcodes":["family_acc"],"annotation":"family: adult, child, child","tags":["family: adult, child, child"],"emoji":"🧑‍🧒‍🧒","order":3473,"group":1,"version":15.1},{"shortcodes":["footprints"],"annotation":"footprints","tags":["clothing","footprint","print"],"emoji":"👣","order":3474,"group":1,"version":0.6},{"shortcodes":["tone1","tone_light"],"annotation":"light skin tone","tags":["skin tone","type 1–2"],"emoji":"🏻","order":3475,"group":2,"version":1},{"shortcodes":["tone2","tone_medium_light"],"annotation":"medium-light skin tone","tags":["skin tone","type 3"],"emoji":"🏼","order":3476,"group":2,"version":1},{"shortcodes":["tone3","tone_medium"],"annotation":"medium skin tone","tags":["skin tone","type 4"],"emoji":"🏽","order":3477,"group":2,"version":1},{"shortcodes":["tone4","tone_medium_dark"],"annotation":"medium-dark skin tone","tags":["skin tone","type 5"],"emoji":"🏾","order":3478,"group":2,"version":1},{"shortcodes":["tone5","tone_dark"],"annotation":"dark skin tone","tags":["skin tone","type 6"],"emoji":"🏿","order":3479,"group":2,"version":1},{"shortcodes":["red_hair"],"annotation":"red hair","tags":["ginger","redhead"],"emoji":"🦰","order":3480,"group":2,"version":11},{"shortcodes":["curly_hair"],"annotation":"curly hair","tags":["afro","curly","ringlets"],"emoji":"🦱","order":3481,"group":2,"version":11},{"shortcodes":["white_hair"],"annotation":"white hair","tags":["gray","hair","old","white"],"emoji":"🦳","order":3482,"group":2,"version":11},{"shortcodes":["no_hair"],"annotation":"bald","tags":["chemotherapy","hairless","no hair","shaven"],"emoji":"🦲","order":3483,"group":2,"version":11},{"shortcodes":["monkey_face"],"annotation":"monkey face","tags":["face","monkey"],"emoji":"🐵","order":3484,"group":3,"version":0.6},{"shortcodes":["monkey"],"annotation":"monkey","tags":["monkey"],"emoji":"🐒","order":3485,"group":3,"version":0.6},{"shortcodes":["gorilla"],"annotation":"gorilla","tags":["gorilla"],"emoji":"🦍","order":3486,"group":3,"version":3},{"shortcodes":["orangutan"],"annotation":"orangutan","tags":["ape"],"emoji":"🦧","order":3487,"group":3,"version":12},{"shortcodes":["dog_face"],"annotation":"dog face","tags":["dog","face","pet"],"emoji":"🐶","order":3488,"group":3,"version":0.6},{"shortcodes":["dog"],"annotation":"dog","tags":["pet"],"emoji":"🐕️","order":3489,"group":3,"version":0.7},{"shortcodes":["guide_dog"],"annotation":"guide dog","tags":["accessibility","blind","guide"],"emoji":"🦮","order":3490,"group":3,"version":12},{"shortcodes":["service_dog"],"annotation":"service dog","tags":["accessibility","assistance","dog","service"],"emoji":"🐕‍🦺","order":3491,"group":3,"version":12},{"shortcodes":["poodle"],"annotation":"poodle","tags":["dog"],"emoji":"🐩","order":3492,"group":3,"version":0.6},{"shortcodes":["wolf","wolf_face"],"annotation":"wolf","tags":["face"],"emoji":"🐺","order":3493,"group":3,"version":0.6},{"shortcodes":["fox","fox_face"],"annotation":"fox","tags":["face"],"emoji":"🦊","order":3494,"group":3,"version":3},{"shortcodes":["raccoon"],"annotation":"raccoon","tags":["curious","sly"],"emoji":"🦝","order":3495,"group":3,"version":11},{"shortcodes":["cat_face"],"annotation":"cat face","tags":["cat","face","pet"],"emoji":"🐱","order":3496,"group":3,"version":0.6},{"shortcodes":["cat"],"annotation":"cat","tags":["pet"],"emoji":"🐈️","order":3497,"group":3,"version":0.7},{"shortcodes":["black_cat"],"annotation":"black cat","tags":["black","cat","unlucky"],"emoji":"🐈‍⬛","order":3498,"group":3,"version":13},{"shortcodes":["lion","lion_face"],"annotation":"lion","tags":["face","leo","zodiac"],"emoji":"🦁","order":3499,"group":3,"version":1},{"shortcodes":["tiger_face"],"annotation":"tiger face","tags":["face","tiger"],"emoji":"🐯","order":3500,"group":3,"version":0.6},{"shortcodes":["tiger"],"annotation":"tiger","tags":["tiger"],"emoji":"🐅","order":3501,"group":3,"version":1},{"shortcodes":["leopard"],"annotation":"leopard","tags":["leopard"],"emoji":"🐆","order":3502,"group":3,"version":1},{"shortcodes":["horse_face"],"annotation":"horse face","tags":["face","horse"],"emoji":"🐴","order":3503,"group":3,"version":0.6},{"shortcodes":["moose"],"annotation":"moose","tags":["animal","antlers","elk","mammal"],"emoji":"🫎","order":3504,"group":3,"version":15},{"shortcodes":["donkey"],"annotation":"donkey","tags":["animal","ass","burro","mammal","mule","stubborn"],"emoji":"🫏","order":3505,"group":3,"version":15},{"shortcodes":["horse","racehorse"],"annotation":"horse","tags":["equestrian","racehorse","racing"],"emoji":"🐎","order":3506,"group":3,"version":0.6},{"shortcodes":["unicorn","unicorn_face"],"annotation":"unicorn","tags":["face"],"emoji":"🦄","order":3507,"group":3,"version":1},{"shortcodes":["zebra"],"annotation":"zebra","tags":["stripe"],"emoji":"🦓","order":3508,"group":3,"version":5},{"shortcodes":["deer"],"annotation":"deer","tags":["deer"],"emoji":"🦌","order":3509,"group":3,"version":3},{"shortcodes":["bison"],"annotation":"bison","tags":["buffalo","herd","wisent"],"emoji":"🦬","order":3510,"group":3,"version":13},{"shortcodes":["cow_face"],"annotation":"cow face","tags":["cow","face"],"emoji":"🐮","order":3511,"group":3,"version":0.6},{"shortcodes":["ox"],"annotation":"ox","tags":["bull","taurus","zodiac"],"emoji":"🐂","order":3512,"group":3,"version":1},{"shortcodes":["water_buffalo"],"annotation":"water buffalo","tags":["buffalo","water"],"emoji":"🐃","order":3513,"group":3,"version":1},{"shortcodes":["cow"],"annotation":"cow","tags":["cow"],"emoji":"🐄","order":3514,"group":3,"version":1},{"shortcodes":["pig_face"],"annotation":"pig face","tags":["face","pig"],"emoji":"🐷","order":3515,"group":3,"version":0.6},{"shortcodes":["pig"],"annotation":"pig","tags":["sow"],"emoji":"🐖","order":3516,"group":3,"version":1},{"shortcodes":["boar"],"annotation":"boar","tags":["pig"],"emoji":"🐗","order":3517,"group":3,"version":0.6},{"shortcodes":["pig_nose"],"annotation":"pig nose","tags":["face","nose","pig"],"emoji":"🐽","order":3518,"group":3,"version":0.6},{"shortcodes":["ram"],"annotation":"ram","tags":["aries","male","sheep","zodiac"],"emoji":"🐏","order":3519,"group":3,"version":1},{"shortcodes":["ewe","sheep"],"annotation":"ewe","tags":["female","sheep"],"emoji":"🐑","order":3520,"group":3,"version":0.6},{"shortcodes":["goat"],"annotation":"goat","tags":["capricorn","zodiac"],"emoji":"🐐","order":3521,"group":3,"version":1},{"shortcodes":["dromedary_camel"],"annotation":"camel","tags":["dromedary","hump"],"emoji":"🐪","order":3522,"group":3,"version":1},{"shortcodes":["camel"],"annotation":"two-hump camel","tags":["bactrian","camel","hump"],"emoji":"🐫","order":3523,"group":3,"version":0.6},{"shortcodes":["llama"],"annotation":"llama","tags":["alpaca","guanaco","vicuña","wool"],"emoji":"🦙","order":3524,"group":3,"version":11},{"shortcodes":["giraffe"],"annotation":"giraffe","tags":["spots"],"emoji":"🦒","order":3525,"group":3,"version":5},{"shortcodes":["elephant"],"annotation":"elephant","tags":["elephant"],"emoji":"🐘","order":3526,"group":3,"version":0.6},{"shortcodes":["mammoth"],"annotation":"mammoth","tags":["extinction","large","tusk","woolly"],"emoji":"🦣","order":3527,"group":3,"version":13},{"shortcodes":["rhino","rhinoceros"],"annotation":"rhinoceros","tags":["rhinoceros"],"emoji":"🦏","order":3528,"group":3,"version":3},{"shortcodes":["hippo"],"annotation":"hippopotamus","tags":["hippo"],"emoji":"🦛","order":3529,"group":3,"version":11},{"shortcodes":["mouse_face"],"annotation":"mouse face","tags":["face","mouse"],"emoji":"🐭","order":3530,"group":3,"version":0.6},{"shortcodes":["mouse"],"annotation":"mouse","tags":["mouse"],"emoji":"🐁","order":3531,"group":3,"version":1},{"shortcodes":["rat"],"annotation":"rat","tags":["rat"],"emoji":"🐀","order":3532,"group":3,"version":1},{"shortcodes":["hamster","hamster_face"],"annotation":"hamster","tags":["face","pet"],"emoji":"🐹","order":3533,"group":3,"version":0.6},{"shortcodes":["rabbit_face"],"annotation":"rabbit face","tags":["bunny","face","pet","rabbit"],"emoji":"🐰","order":3534,"group":3,"version":0.6},{"shortcodes":["rabbit"],"annotation":"rabbit","tags":["bunny","pet"],"emoji":"🐇","order":3535,"group":3,"version":1},{"shortcodes":["chipmunk"],"annotation":"chipmunk","tags":["squirrel"],"emoji":"🐿️","order":3537,"group":3,"version":0.7},{"shortcodes":["beaver"],"annotation":"beaver","tags":["dam"],"emoji":"🦫","order":3538,"group":3,"version":13},{"shortcodes":["hedgehog"],"annotation":"hedgehog","tags":["spiny"],"emoji":"🦔","order":3539,"group":3,"version":5},{"shortcodes":["bat"],"annotation":"bat","tags":["vampire"],"emoji":"🦇","order":3540,"group":3,"version":3},{"shortcodes":["bear","bear_face"],"annotation":"bear","tags":["face"],"emoji":"🐻","order":3541,"group":3,"version":0.6},{"shortcodes":["polar_bear","polar_bear_face"],"annotation":"polar bear","tags":["arctic","bear","white"],"emoji":"🐻‍❄️","order":3542,"group":3,"version":13},{"shortcodes":["koala","koala_face"],"annotation":"koala","tags":["face","marsupial"],"emoji":"🐨","order":3544,"group":3,"version":0.6},{"shortcodes":["panda","panda_face"],"annotation":"panda","tags":["face"],"emoji":"🐼","order":3545,"group":3,"version":0.6},{"shortcodes":["sloth"],"annotation":"sloth","tags":["lazy","slow"],"emoji":"🦥","order":3546,"group":3,"version":12},{"shortcodes":["otter"],"annotation":"otter","tags":["fishing","playful"],"emoji":"🦦","order":3547,"group":3,"version":12},{"shortcodes":["skunk"],"annotation":"skunk","tags":["stink"],"emoji":"🦨","order":3548,"group":3,"version":12},{"shortcodes":["kangaroo"],"annotation":"kangaroo","tags":["joey","jump","marsupial"],"emoji":"🦘","order":3549,"group":3,"version":11},{"shortcodes":["badger"],"annotation":"badger","tags":["honey badger","pester"],"emoji":"🦡","order":3550,"group":3,"version":11},{"shortcodes":["paw_prints"],"annotation":"paw prints","tags":["feet","paw","print"],"emoji":"🐾","order":3551,"group":3,"version":0.6},{"shortcodes":["turkey"],"annotation":"turkey","tags":["bird"],"emoji":"🦃","order":3552,"group":3,"version":1},{"shortcodes":["chicken","chicken_face"],"annotation":"chicken","tags":["bird"],"emoji":"🐔","order":3553,"group":3,"version":0.6},{"shortcodes":["rooster"],"annotation":"rooster","tags":["bird"],"emoji":"🐓","order":3554,"group":3,"version":1},{"shortcodes":["hatching_chick"],"annotation":"hatching chick","tags":["baby","bird","chick","hatching"],"emoji":"🐣","order":3555,"group":3,"version":0.6},{"shortcodes":["baby_chick"],"annotation":"baby chick","tags":["baby","bird","chick"],"emoji":"🐤","order":3556,"group":3,"version":0.6},{"shortcodes":["hatched_chick"],"annotation":"front-facing baby chick","tags":["baby","bird","chick"],"emoji":"🐥","order":3557,"group":3,"version":0.6},{"shortcodes":["bird","bird_face"],"annotation":"bird","tags":["bird"],"emoji":"🐦️","order":3558,"group":3,"version":0.6},{"shortcodes":["penguin","penguin_face"],"annotation":"penguin","tags":["bird"],"emoji":"🐧","order":3559,"group":3,"version":0.6},{"shortcodes":["dove"],"annotation":"dove","tags":["bird","fly","peace"],"emoji":"🕊️","order":3561,"group":3,"version":0.7},{"shortcodes":["eagle"],"annotation":"eagle","tags":["bird"],"emoji":"🦅","order":3562,"group":3,"version":3},{"shortcodes":["duck"],"annotation":"duck","tags":["bird"],"emoji":"🦆","order":3563,"group":3,"version":3},{"shortcodes":["swan"],"annotation":"swan","tags":["bird","cygnet","ugly duckling"],"emoji":"🦢","order":3564,"group":3,"version":11},{"shortcodes":["owl"],"annotation":"owl","tags":["bird","wise"],"emoji":"🦉","order":3565,"group":3,"version":3},{"shortcodes":["dodo"],"annotation":"dodo","tags":["extinction","large","mauritius"],"emoji":"🦤","order":3566,"group":3,"version":13},{"shortcodes":["feather"],"annotation":"feather","tags":["bird","flight","light","plumage"],"emoji":"🪶","order":3567,"group":3,"version":13},{"shortcodes":["flamingo"],"annotation":"flamingo","tags":["flamboyant","tropical"],"emoji":"🦩","order":3568,"group":3,"version":12},{"shortcodes":["peacock"],"annotation":"peacock","tags":["bird","ostentatious","peahen","proud"],"emoji":"🦚","order":3569,"group":3,"version":11},{"shortcodes":["parrot"],"annotation":"parrot","tags":["bird","pirate","talk"],"emoji":"🦜","order":3570,"group":3,"version":11},{"shortcodes":["wing"],"annotation":"wing","tags":["angelic","aviation","bird","flying","mythology"],"emoji":"🪽","order":3571,"group":3,"version":15},{"shortcodes":["black_bird"],"annotation":"black bird","tags":["bird","black","crow","raven","rook"],"emoji":"🐦‍⬛","order":3572,"group":3,"version":15},{"shortcodes":["goose"],"annotation":"goose","tags":["bird","fowl","honk","silly"],"emoji":"🪿","order":3573,"group":3,"version":15},{"shortcodes":["phoenix"],"annotation":"phoenix","tags":["fantasy","firebird","rebirth","reincarnation"],"emoji":"🐦‍🔥","order":3574,"group":3,"version":15.1},{"shortcodes":["frog","frog_face"],"annotation":"frog","tags":["face"],"emoji":"🐸","order":3575,"group":3,"version":0.6},{"shortcodes":["crocodile"],"annotation":"crocodile","tags":["crocodile"],"emoji":"🐊","order":3576,"group":3,"version":1},{"shortcodes":["turtle"],"annotation":"turtle","tags":["terrapin","tortoise"],"emoji":"🐢","order":3577,"group":3,"version":0.6},{"shortcodes":["lizard"],"annotation":"lizard","tags":["reptile"],"emoji":"🦎","order":3578,"group":3,"version":3},{"shortcodes":["snake"],"annotation":"snake","tags":["bearer","ophiuchus","serpent","zodiac"],"emoji":"🐍","order":3579,"group":3,"version":0.6},{"shortcodes":["dragon_face"],"annotation":"dragon face","tags":["dragon","face","fairy tale"],"emoji":"🐲","order":3580,"group":3,"version":0.6},{"shortcodes":["dragon"],"annotation":"dragon","tags":["fairy tale"],"emoji":"🐉","order":3581,"group":3,"version":1},{"shortcodes":["sauropod"],"annotation":"sauropod","tags":["brachiosaurus","brontosaurus","diplodocus"],"emoji":"🦕","order":3582,"group":3,"version":5},{"shortcodes":["t-rex","trex"],"annotation":"T-Rex","tags":["t-rex","tyrannosaurus rex"],"emoji":"🦖","order":3583,"group":3,"version":5},{"shortcodes":["spouting_whale"],"annotation":"spouting whale","tags":["face","spouting","whale"],"emoji":"🐳","order":3584,"group":3,"version":0.6},{"shortcodes":["whale"],"annotation":"whale","tags":["whale"],"emoji":"🐋","order":3585,"group":3,"version":1},{"shortcodes":["dolphin"],"annotation":"dolphin","tags":["flipper"],"emoji":"🐬","order":3586,"group":3,"version":0.6},{"shortcodes":["seal"],"annotation":"seal","tags":["sea lion"],"emoji":"🦭","order":3587,"group":3,"version":13},{"shortcodes":["fish"],"annotation":"fish","tags":["pisces","zodiac"],"emoji":"🐟️","order":3588,"group":3,"version":0.6},{"shortcodes":["tropical_fish"],"annotation":"tropical fish","tags":["fish","tropical"],"emoji":"🐠","order":3589,"group":3,"version":0.6},{"shortcodes":["blowfish"],"annotation":"blowfish","tags":["fish"],"emoji":"🐡","order":3590,"group":3,"version":0.6},{"shortcodes":["shark"],"annotation":"shark","tags":["fish"],"emoji":"🦈","order":3591,"group":3,"version":3},{"shortcodes":["octopus"],"annotation":"octopus","tags":["octopus"],"emoji":"🐙","order":3592,"group":3,"version":0.6},{"shortcodes":["shell"],"annotation":"spiral shell","tags":["shell","spiral"],"emoji":"🐚","order":3593,"group":3,"version":0.6},{"shortcodes":["coral"],"annotation":"coral","tags":["ocean","reef"],"emoji":"🪸","order":3594,"group":3,"version":14},{"shortcodes":["jellyfish"],"annotation":"jellyfish","tags":["burn","invertebrate","jelly","marine","ouch","stinger"],"emoji":"🪼","order":3595,"group":3,"version":15},{"shortcodes":["snail"],"annotation":"snail","tags":["snail"],"emoji":"🐌","order":3596,"group":3,"version":0.6},{"shortcodes":["butterfly"],"annotation":"butterfly","tags":["insect","pretty"],"emoji":"🦋","order":3597,"group":3,"version":3},{"shortcodes":["bug"],"annotation":"bug","tags":["insect"],"emoji":"🐛","order":3598,"group":3,"version":0.6},{"shortcodes":["ant"],"annotation":"ant","tags":["insect"],"emoji":"🐜","order":3599,"group":3,"version":0.6},{"shortcodes":["bee"],"annotation":"honeybee","tags":["bee","insect"],"emoji":"🐝","order":3600,"group":3,"version":0.6},{"shortcodes":["beetle"],"annotation":"beetle","tags":["bug","insect"],"emoji":"🪲","order":3601,"group":3,"version":13},{"shortcodes":["lady_beetle"],"annotation":"lady beetle","tags":["beetle","insect","ladybird","ladybug"],"emoji":"🐞","order":3602,"group":3,"version":0.6},{"shortcodes":["cricket"],"annotation":"cricket","tags":["grasshopper"],"emoji":"🦗","order":3603,"group":3,"version":5},{"shortcodes":["cockroach"],"annotation":"cockroach","tags":["insect","pest","roach"],"emoji":"🪳","order":3604,"group":3,"version":13},{"shortcodes":["spider"],"annotation":"spider","tags":["insect"],"emoji":"🕷️","order":3606,"group":3,"version":0.7},{"shortcodes":["spider_web"],"annotation":"spider web","tags":["spider","web"],"emoji":"🕸️","order":3608,"group":3,"version":0.7},{"shortcodes":["scorpion"],"annotation":"scorpion","tags":["scorpio","zodiac"],"emoji":"🦂","order":3609,"group":3,"version":1},{"shortcodes":["mosquito"],"annotation":"mosquito","tags":["disease","fever","malaria","pest","virus"],"emoji":"🦟","order":3610,"group":3,"version":11},{"shortcodes":["fly"],"annotation":"fly","tags":["disease","maggot","pest","rotting"],"emoji":"🪰","order":3611,"group":3,"version":13},{"shortcodes":["worm"],"annotation":"worm","tags":["annelid","earthworm","parasite"],"emoji":"🪱","order":3612,"group":3,"version":13},{"shortcodes":["microbe"],"annotation":"microbe","tags":["amoeba","bacteria","virus"],"emoji":"🦠","order":3613,"group":3,"version":11},{"shortcodes":["bouquet"],"annotation":"bouquet","tags":["flower"],"emoji":"💐","order":3614,"group":3,"version":0.6},{"shortcodes":["cherry_blossom"],"annotation":"cherry blossom","tags":["blossom","cherry","flower"],"emoji":"🌸","order":3615,"group":3,"version":0.6},{"shortcodes":["white_flower"],"annotation":"white flower","tags":["flower"],"emoji":"💮","order":3616,"group":3,"version":0.6},{"shortcodes":["lotus"],"annotation":"lotus","tags":["buddhism","flower","hinduism","purity"],"emoji":"🪷","order":3617,"group":3,"version":14},{"shortcodes":["rosette"],"annotation":"rosette","tags":["plant"],"emoji":"🏵️","order":3619,"group":3,"version":0.7},{"shortcodes":["rose"],"annotation":"rose","tags":["flower"],"emoji":"🌹","order":3620,"group":3,"version":0.6},{"shortcodes":["wilted_flower"],"annotation":"wilted flower","tags":["flower","wilted"],"emoji":"🥀","order":3621,"group":3,"version":3},{"shortcodes":["hibiscus"],"annotation":"hibiscus","tags":["flower"],"emoji":"🌺","order":3622,"group":3,"version":0.6},{"shortcodes":["sunflower"],"annotation":"sunflower","tags":["flower","sun"],"emoji":"🌻","order":3623,"group":3,"version":0.6},{"shortcodes":["blossom"],"annotation":"blossom","tags":["flower"],"emoji":"🌼","order":3624,"group":3,"version":0.6},{"shortcodes":["tulip"],"annotation":"tulip","tags":["flower"],"emoji":"🌷","order":3625,"group":3,"version":0.6},{"shortcodes":["hyacinth"],"annotation":"hyacinth","tags":["bluebonnet","flower","lavender","lupine","snapdragon"],"emoji":"🪻","order":3626,"group":3,"version":15},{"shortcodes":["seedling"],"annotation":"seedling","tags":["young"],"emoji":"🌱","order":3627,"group":3,"version":0.6},{"shortcodes":["potted_plant"],"annotation":"potted plant","tags":["boring","grow","house","nurturing","plant","useless"],"emoji":"🪴","order":3628,"group":3,"version":13},{"shortcodes":["evergreen_tree"],"annotation":"evergreen tree","tags":["tree"],"emoji":"🌲","order":3629,"group":3,"version":1},{"shortcodes":["deciduous_tree"],"annotation":"deciduous tree","tags":["deciduous","shedding","tree"],"emoji":"🌳","order":3630,"group":3,"version":1},{"shortcodes":["palm_tree"],"annotation":"palm tree","tags":["palm","tree"],"emoji":"🌴","order":3631,"group":3,"version":0.6},{"shortcodes":["cactus"],"annotation":"cactus","tags":["plant"],"emoji":"🌵","order":3632,"group":3,"version":0.6},{"shortcodes":["ear_of_rice","sheaf_of_rice"],"annotation":"sheaf of rice","tags":["ear","grain","rice"],"emoji":"🌾","order":3633,"group":3,"version":0.6},{"shortcodes":["herb"],"annotation":"herb","tags":["leaf"],"emoji":"🌿","order":3634,"group":3,"version":0.6},{"shortcodes":["shamrock"],"annotation":"shamrock","tags":["plant"],"emoji":"☘️","order":3636,"group":3,"version":1},{"shortcodes":["four_leaf_clover"],"annotation":"four leaf clover","tags":["4","clover","four","four-leaf clover","leaf"],"emoji":"🍀","order":3637,"group":3,"version":0.6},{"shortcodes":["maple_leaf"],"annotation":"maple leaf","tags":["falling","leaf","maple"],"emoji":"🍁","order":3638,"group":3,"version":0.6},{"shortcodes":["fallen_leaf"],"annotation":"fallen leaf","tags":["falling","leaf"],"emoji":"🍂","order":3639,"group":3,"version":0.6},{"shortcodes":["leaves"],"annotation":"leaf fluttering in wind","tags":["blow","flutter","leaf","wind"],"emoji":"🍃","order":3640,"group":3,"version":0.6},{"shortcodes":["empty_nest","nest"],"annotation":"empty nest","tags":["nesting"],"emoji":"🪹","order":3641,"group":3,"version":14},{"shortcodes":["nest_with_eggs"],"annotation":"nest with eggs","tags":["nesting"],"emoji":"🪺","order":3642,"group":3,"version":14},{"shortcodes":["mushroom"],"annotation":"mushroom","tags":["toadstool"],"emoji":"🍄","order":3643,"group":3,"version":0.6},{"shortcodes":["grapes"],"annotation":"grapes","tags":["fruit","grape"],"emoji":"🍇","order":3644,"group":4,"version":0.6},{"shortcodes":["melon"],"annotation":"melon","tags":["fruit"],"emoji":"🍈","order":3645,"group":4,"version":0.6},{"shortcodes":["watermelon"],"annotation":"watermelon","tags":["fruit"],"emoji":"🍉","order":3646,"group":4,"version":0.6},{"shortcodes":["orange","tangerine"],"annotation":"tangerine","tags":["fruit","orange"],"emoji":"🍊","order":3647,"group":4,"version":0.6},{"shortcodes":["lemon"],"annotation":"lemon","tags":["citrus","fruit"],"emoji":"🍋","order":3648,"group":4,"version":1},{"shortcodes":["lime"],"annotation":"lime","tags":["citrus","fruit","tropical"],"emoji":"🍋‍🟩","order":3649,"group":4,"version":15.1},{"shortcodes":["banana"],"annotation":"banana","tags":["fruit"],"emoji":"🍌","order":3650,"group":4,"version":0.6},{"shortcodes":["pineapple"],"annotation":"pineapple","tags":["fruit"],"emoji":"🍍","order":3651,"group":4,"version":0.6},{"shortcodes":["mango"],"annotation":"mango","tags":["fruit","tropical"],"emoji":"🥭","order":3652,"group":4,"version":11},{"shortcodes":["apple","red_apple"],"annotation":"red apple","tags":["apple","fruit","red"],"emoji":"🍎","order":3653,"group":4,"version":0.6},{"shortcodes":["green_apple"],"annotation":"green apple","tags":["apple","fruit","green"],"emoji":"🍏","order":3654,"group":4,"version":0.6},{"shortcodes":["pear"],"annotation":"pear","tags":["fruit"],"emoji":"🍐","order":3655,"group":4,"version":1},{"shortcodes":["peach"],"annotation":"peach","tags":["fruit"],"emoji":"🍑","order":3656,"group":4,"version":0.6},{"shortcodes":["cherries"],"annotation":"cherries","tags":["berries","cherry","fruit","red"],"emoji":"🍒","order":3657,"group":4,"version":0.6},{"shortcodes":["strawberry"],"annotation":"strawberry","tags":["berry","fruit"],"emoji":"🍓","order":3658,"group":4,"version":0.6},{"shortcodes":["blueberries"],"annotation":"blueberries","tags":["berry","bilberry","blue","blueberry"],"emoji":"🫐","order":3659,"group":4,"version":13},{"shortcodes":["kiwi"],"annotation":"kiwi fruit","tags":["food","fruit","kiwi"],"emoji":"🥝","order":3660,"group":4,"version":3},{"shortcodes":["tomato"],"annotation":"tomato","tags":["fruit","vegetable"],"emoji":"🍅","order":3661,"group":4,"version":0.6},{"shortcodes":["olive"],"annotation":"olive","tags":["food"],"emoji":"🫒","order":3662,"group":4,"version":13},{"shortcodes":["coconut"],"annotation":"coconut","tags":["palm","piña colada"],"emoji":"🥥","order":3663,"group":4,"version":5},{"shortcodes":["avocado"],"annotation":"avocado","tags":["food","fruit"],"emoji":"🥑","order":3664,"group":4,"version":3},{"shortcodes":["eggplant"],"annotation":"eggplant","tags":["aubergine","vegetable"],"emoji":"🍆","order":3665,"group":4,"version":0.6},{"shortcodes":["potato"],"annotation":"potato","tags":["food","vegetable"],"emoji":"🥔","order":3666,"group":4,"version":3},{"shortcodes":["carrot"],"annotation":"carrot","tags":["food","vegetable"],"emoji":"🥕","order":3667,"group":4,"version":3},{"shortcodes":["corn","ear_of_corn"],"annotation":"ear of corn","tags":["corn","ear","maize","maze"],"emoji":"🌽","order":3668,"group":4,"version":0.6},{"shortcodes":["hot_pepper"],"annotation":"hot pepper","tags":["hot","pepper"],"emoji":"🌶️","order":3670,"group":4,"version":0.7},{"shortcodes":["bell_pepper"],"annotation":"bell pepper","tags":["capsicum","pepper","vegetable"],"emoji":"🫑","order":3671,"group":4,"version":13},{"shortcodes":["cucumber"],"annotation":"cucumber","tags":["food","pickle","vegetable"],"emoji":"🥒","order":3672,"group":4,"version":3},{"shortcodes":["leafy_green"],"annotation":"leafy green","tags":["bok choy","cabbage","kale","lettuce"],"emoji":"🥬","order":3673,"group":4,"version":11},{"shortcodes":["broccoli"],"annotation":"broccoli","tags":["wild cabbage"],"emoji":"🥦","order":3674,"group":4,"version":5},{"shortcodes":["garlic"],"annotation":"garlic","tags":["flavoring"],"emoji":"🧄","order":3675,"group":4,"version":12},{"shortcodes":["onion"],"annotation":"onion","tags":["flavoring"],"emoji":"🧅","order":3676,"group":4,"version":12},{"shortcodes":["peanuts"],"annotation":"peanuts","tags":["food","nut","peanut","vegetable"],"emoji":"🥜","order":3677,"group":4,"version":3},{"shortcodes":["beans"],"annotation":"beans","tags":["food","kidney","legume"],"emoji":"🫘","order":3678,"group":4,"version":14},{"shortcodes":["chestnut"],"annotation":"chestnut","tags":["plant"],"emoji":"🌰","order":3679,"group":4,"version":0.6},{"shortcodes":["ginger"],"annotation":"ginger root","tags":["beer","root","spice"],"emoji":"🫚","order":3680,"group":4,"version":15},{"shortcodes":["pea"],"annotation":"pea pod","tags":["beans","edamame","legume","pea","pod","vegetable"],"emoji":"🫛","order":3681,"group":4,"version":15},{"shortcodes":["brown_mushroom"],"annotation":"brown mushroom","tags":["food","fungus","nature","vegetable"],"emoji":"🍄‍🟫","order":3682,"group":4,"version":15.1},{"shortcodes":["bread"],"annotation":"bread","tags":["loaf"],"emoji":"🍞","order":3683,"group":4,"version":0.6},{"shortcodes":["croissant"],"annotation":"croissant","tags":["bread","breakfast","food","french","roll"],"emoji":"🥐","order":3684,"group":4,"version":3},{"shortcodes":["baguette_bread"],"annotation":"baguette bread","tags":["baguette","bread","food","french"],"emoji":"🥖","order":3685,"group":4,"version":3},{"shortcodes":["flatbread"],"annotation":"flatbread","tags":["arepa","lavash","naan","pita"],"emoji":"🫓","order":3686,"group":4,"version":13},{"shortcodes":["pretzel"],"annotation":"pretzel","tags":["twisted"],"emoji":"🥨","order":3687,"group":4,"version":5},{"shortcodes":["bagel"],"annotation":"bagel","tags":["bakery","breakfast","schmear"],"emoji":"🥯","order":3688,"group":4,"version":11},{"shortcodes":["pancakes"],"annotation":"pancakes","tags":["breakfast","crêpe","food","hotcake","pancake"],"emoji":"🥞","order":3689,"group":4,"version":3},{"shortcodes":["waffle"],"annotation":"waffle","tags":["breakfast","indecisive","iron"],"emoji":"🧇","order":3690,"group":4,"version":12},{"shortcodes":["cheese"],"annotation":"cheese wedge","tags":["cheese"],"emoji":"🧀","order":3691,"group":4,"version":1},{"shortcodes":["meat_on_bone"],"annotation":"meat on bone","tags":["bone","meat"],"emoji":"🍖","order":3692,"group":4,"version":0.6},{"shortcodes":["poultry_leg"],"annotation":"poultry leg","tags":["bone","chicken","drumstick","leg","poultry"],"emoji":"🍗","order":3693,"group":4,"version":0.6},{"shortcodes":["cut_of_meat"],"annotation":"cut of meat","tags":["chop","lambchop","porkchop","steak"],"emoji":"🥩","order":3694,"group":4,"version":5},{"shortcodes":["bacon"],"annotation":"bacon","tags":["breakfast","food","meat"],"emoji":"🥓","order":3695,"group":4,"version":3},{"shortcodes":["hamburger"],"annotation":"hamburger","tags":["burger"],"emoji":"🍔","order":3696,"group":4,"version":0.6},{"shortcodes":["french_fries","fries"],"annotation":"french fries","tags":["french","fries"],"emoji":"🍟","order":3697,"group":4,"version":0.6},{"shortcodes":["pizza"],"annotation":"pizza","tags":["cheese","slice"],"emoji":"🍕","order":3698,"group":4,"version":0.6},{"shortcodes":["hotdog"],"annotation":"hot dog","tags":["frankfurter","hotdog","sausage"],"emoji":"🌭","order":3699,"group":4,"version":1},{"shortcodes":["sandwich"],"annotation":"sandwich","tags":["bread"],"emoji":"🥪","order":3700,"group":4,"version":5},{"shortcodes":["taco"],"annotation":"taco","tags":["mexican"],"emoji":"🌮","order":3701,"group":4,"version":1},{"shortcodes":["burrito"],"annotation":"burrito","tags":["mexican","wrap"],"emoji":"🌯","order":3702,"group":4,"version":1},{"shortcodes":["tamale"],"annotation":"tamale","tags":["mexican","wrapped"],"emoji":"🫔","order":3703,"group":4,"version":13},{"shortcodes":["stuffed_flatbread"],"annotation":"stuffed flatbread","tags":["falafel","flatbread","food","gyro","kebab","stuffed"],"emoji":"🥙","order":3704,"group":4,"version":3},{"shortcodes":["falafel"],"annotation":"falafel","tags":["chickpea","meatball"],"emoji":"🧆","order":3705,"group":4,"version":12},{"shortcodes":["egg"],"annotation":"egg","tags":["breakfast","food"],"emoji":"🥚","order":3706,"group":4,"version":3},{"shortcodes":["cooking","fried_egg"],"annotation":"cooking","tags":["breakfast","egg","frying","pan"],"emoji":"🍳","order":3707,"group":4,"version":0.6},{"shortcodes":["shallow_pan_of_food"],"annotation":"shallow pan of food","tags":["casserole","food","paella","pan","shallow"],"emoji":"🥘","order":3708,"group":4,"version":3},{"shortcodes":["pot_of_food","stew"],"annotation":"pot of food","tags":["pot","stew"],"emoji":"🍲","order":3709,"group":4,"version":0.6},{"shortcodes":["fondue"],"annotation":"fondue","tags":["cheese","chocolate","melted","pot","swiss"],"emoji":"🫕","order":3710,"group":4,"version":13},{"shortcodes":["bowl_with_spoon"],"annotation":"bowl with spoon","tags":["breakfast","cereal","congee"],"emoji":"🥣","order":3711,"group":4,"version":5},{"shortcodes":["green_salad","salad"],"annotation":"green salad","tags":["food","green","salad"],"emoji":"🥗","order":3712,"group":4,"version":3},{"shortcodes":["popcorn"],"annotation":"popcorn","tags":["popcorn"],"emoji":"🍿","order":3713,"group":4,"version":1},{"shortcodes":["butter"],"annotation":"butter","tags":["dairy"],"emoji":"🧈","order":3714,"group":4,"version":12},{"shortcodes":["salt"],"annotation":"salt","tags":["condiment","shaker"],"emoji":"🧂","order":3715,"group":4,"version":11},{"shortcodes":["canned_food"],"annotation":"canned food","tags":["can"],"emoji":"🥫","order":3716,"group":4,"version":5},{"shortcodes":["bento","bento_box"],"annotation":"bento box","tags":["bento","box"],"emoji":"🍱","order":3717,"group":4,"version":0.6},{"shortcodes":["rice_cracker"],"annotation":"rice cracker","tags":["cracker","rice"],"emoji":"🍘","order":3718,"group":4,"version":0.6},{"shortcodes":["rice_ball"],"annotation":"rice ball","tags":["ball","japanese","rice"],"emoji":"🍙","order":3719,"group":4,"version":0.6},{"shortcodes":["cooked_rice","rice"],"annotation":"cooked rice","tags":["cooked","rice"],"emoji":"🍚","order":3720,"group":4,"version":0.6},{"shortcodes":["curry","curry_rice"],"annotation":"curry rice","tags":["curry","rice"],"emoji":"🍛","order":3721,"group":4,"version":0.6},{"shortcodes":["ramen","steaming_bowl"],"annotation":"steaming bowl","tags":["bowl","noodle","ramen","steaming"],"emoji":"🍜","order":3722,"group":4,"version":0.6},{"shortcodes":["spaghetti"],"annotation":"spaghetti","tags":["pasta"],"emoji":"🍝","order":3723,"group":4,"version":0.6},{"shortcodes":["sweet_potato"],"annotation":"roasted sweet potato","tags":["potato","roasted","sweet"],"emoji":"🍠","order":3724,"group":4,"version":0.6},{"shortcodes":["oden"],"annotation":"oden","tags":["kebab","seafood","skewer","stick"],"emoji":"🍢","order":3725,"group":4,"version":0.6},{"shortcodes":["sushi"],"annotation":"sushi","tags":["sushi"],"emoji":"🍣","order":3726,"group":4,"version":0.6},{"shortcodes":["fried_shrimp"],"annotation":"fried shrimp","tags":["fried","prawn","shrimp","tempura"],"emoji":"🍤","order":3727,"group":4,"version":0.6},{"shortcodes":["fish_cake"],"annotation":"fish cake with swirl","tags":["cake","fish","pastry","swirl"],"emoji":"🍥","order":3728,"group":4,"version":0.6},{"shortcodes":["moon_cake"],"annotation":"moon cake","tags":["autumn","festival","yuèbǐng"],"emoji":"🥮","order":3729,"group":4,"version":11},{"shortcodes":["dango"],"annotation":"dango","tags":["dessert","japanese","skewer","stick","sweet"],"emoji":"🍡","order":3730,"group":4,"version":0.6},{"shortcodes":["dumpling"],"annotation":"dumpling","tags":["empanada","gyōza","jiaozi","pierogi","potsticker"],"emoji":"🥟","order":3731,"group":4,"version":5},{"shortcodes":["fortune_cookie"],"annotation":"fortune cookie","tags":["prophecy"],"emoji":"🥠","order":3732,"group":4,"version":5},{"shortcodes":["takeout_box"],"annotation":"takeout box","tags":["oyster pail"],"emoji":"🥡","order":3733,"group":4,"version":5},{"shortcodes":["crab"],"annotation":"crab","tags":["cancer","zodiac"],"emoji":"🦀","order":3734,"group":4,"version":1},{"shortcodes":["lobster"],"annotation":"lobster","tags":["bisque","claws","seafood"],"emoji":"🦞","order":3735,"group":4,"version":11},{"shortcodes":["shrimp"],"annotation":"shrimp","tags":["food","shellfish","small"],"emoji":"🦐","order":3736,"group":4,"version":3},{"shortcodes":["squid"],"annotation":"squid","tags":["food","molusc"],"emoji":"🦑","order":3737,"group":4,"version":3},{"shortcodes":["oyster"],"annotation":"oyster","tags":["diving","pearl"],"emoji":"🦪","order":3738,"group":4,"version":12},{"shortcodes":["icecream","soft_serve"],"annotation":"soft ice cream","tags":["cream","dessert","ice","icecream","soft","sweet"],"emoji":"🍦","order":3739,"group":4,"version":0.6},{"shortcodes":["shaved_ice"],"annotation":"shaved ice","tags":["dessert","ice","shaved","sweet"],"emoji":"🍧","order":3740,"group":4,"version":0.6},{"shortcodes":["ice_cream"],"annotation":"ice cream","tags":["cream","dessert","ice","sweet"],"emoji":"🍨","order":3741,"group":4,"version":0.6},{"shortcodes":["doughnut"],"annotation":"doughnut","tags":["breakfast","dessert","donut","sweet"],"emoji":"🍩","order":3742,"group":4,"version":0.6},{"shortcodes":["cookie"],"annotation":"cookie","tags":["dessert","sweet"],"emoji":"🍪","order":3743,"group":4,"version":0.6},{"shortcodes":["birthday","birthday_cake"],"annotation":"birthday cake","tags":["birthday","cake","celebration","dessert","pastry","sweet"],"emoji":"🎂","order":3744,"group":4,"version":0.6},{"shortcodes":["cake","shortcake"],"annotation":"shortcake","tags":["cake","dessert","pastry","slice","sweet"],"emoji":"🍰","order":3745,"group":4,"version":0.6},{"shortcodes":["cupcake"],"annotation":"cupcake","tags":["bakery","sweet"],"emoji":"🧁","order":3746,"group":4,"version":11},{"shortcodes":["pie"],"annotation":"pie","tags":["filling","pastry"],"emoji":"🥧","order":3747,"group":4,"version":5},{"shortcodes":["chocolate_bar"],"annotation":"chocolate bar","tags":["bar","chocolate","dessert","sweet"],"emoji":"🍫","order":3748,"group":4,"version":0.6},{"shortcodes":["candy"],"annotation":"candy","tags":["dessert","sweet"],"emoji":"🍬","order":3749,"group":4,"version":0.6},{"shortcodes":["lollipop"],"annotation":"lollipop","tags":["candy","dessert","sweet"],"emoji":"🍭","order":3750,"group":4,"version":0.6},{"shortcodes":["custard"],"annotation":"custard","tags":["dessert","pudding","sweet"],"emoji":"🍮","order":3751,"group":4,"version":0.6},{"shortcodes":["honey_pot"],"annotation":"honey pot","tags":["honey","honeypot","pot","sweet"],"emoji":"🍯","order":3752,"group":4,"version":0.6},{"shortcodes":["baby_bottle"],"annotation":"baby bottle","tags":["baby","bottle","drink","milk"],"emoji":"🍼","order":3753,"group":4,"version":1},{"shortcodes":["glass_of_milk","milk"],"annotation":"glass of milk","tags":["drink","glass","milk"],"emoji":"🥛","order":3754,"group":4,"version":3},{"shortcodes":["coffee"],"annotation":"hot beverage","tags":["beverage","coffee","drink","hot","steaming","tea"],"emoji":"☕️","order":3755,"group":4,"version":0.6},{"shortcodes":["teapot"],"annotation":"teapot","tags":["drink","pot","tea"],"emoji":"🫖","order":3756,"group":4,"version":13},{"shortcodes":["tea"],"annotation":"teacup without handle","tags":["beverage","cup","drink","tea","teacup"],"emoji":"🍵","order":3757,"group":4,"version":0.6},{"shortcodes":["sake"],"annotation":"sake","tags":["bar","beverage","bottle","cup","drink"],"emoji":"🍶","order":3758,"group":4,"version":0.6},{"shortcodes":["champagne"],"annotation":"bottle with popping cork","tags":["bar","bottle","cork","drink","popping"],"emoji":"🍾","order":3759,"group":4,"version":1},{"shortcodes":["wine_glass"],"annotation":"wine glass","tags":["bar","beverage","drink","glass","wine"],"emoji":"🍷","order":3760,"group":4,"version":0.6},{"shortcodes":["cocktail"],"annotation":"cocktail glass","tags":["bar","cocktail","drink","glass"],"emoji":"🍸️","order":3761,"group":4,"version":0.6},{"shortcodes":["tropical_drink"],"annotation":"tropical drink","tags":["bar","drink","tropical"],"emoji":"🍹","order":3762,"group":4,"version":0.6},{"shortcodes":["beer"],"annotation":"beer mug","tags":["bar","beer","drink","mug"],"emoji":"🍺","order":3763,"group":4,"version":0.6},{"shortcodes":["beers"],"annotation":"clinking beer mugs","tags":["bar","beer","clink","drink","mug"],"emoji":"🍻","order":3764,"group":4,"version":0.6},{"shortcodes":["clinking_glasses"],"annotation":"clinking glasses","tags":["celebrate","clink","drink","glass"],"emoji":"🥂","order":3765,"group":4,"version":3},{"shortcodes":["tumbler_glass","whisky"],"annotation":"tumbler glass","tags":["glass","liquor","shot","tumbler","whisky"],"emoji":"🥃","order":3766,"group":4,"version":3},{"shortcodes":["pour","pouring_liquid"],"annotation":"pouring liquid","tags":["drink","empty","glass","spill"],"emoji":"🫗","order":3767,"group":4,"version":14},{"shortcodes":["cup_with_straw"],"annotation":"cup with straw","tags":["juice","soda"],"emoji":"🥤","order":3768,"group":4,"version":5},{"shortcodes":["boba_drink","bubble_tea"],"annotation":"bubble tea","tags":["bubble","milk","pearl","tea"],"emoji":"🧋","order":3769,"group":4,"version":13},{"shortcodes":["beverage_box","juice_box"],"annotation":"beverage box","tags":["beverage","box","juice","straw","sweet"],"emoji":"🧃","order":3770,"group":4,"version":12},{"shortcodes":["mate"],"annotation":"mate","tags":["drink"],"emoji":"🧉","order":3771,"group":4,"version":12},{"shortcodes":["ice","ice_cube"],"annotation":"ice","tags":["cold","ice cube","iceberg"],"emoji":"🧊","order":3772,"group":4,"version":12},{"shortcodes":["chopsticks"],"annotation":"chopsticks","tags":["hashi"],"emoji":"🥢","order":3773,"group":4,"version":5},{"shortcodes":["fork_knife_plate"],"annotation":"fork and knife with plate","tags":["cooking","fork","knife","plate"],"emoji":"🍽️","order":3775,"group":4,"version":0.7},{"shortcodes":["fork_and_knife"],"annotation":"fork and knife","tags":["cooking","cutlery","fork","knife"],"emoji":"🍴","order":3776,"group":4,"version":0.6},{"shortcodes":["spoon"],"annotation":"spoon","tags":["tableware"],"emoji":"🥄","order":3777,"group":4,"version":3},{"shortcodes":["knife"],"annotation":"kitchen knife","tags":["cooking","hocho","knife","tool","weapon"],"emoji":"🔪","order":3778,"group":4,"version":0.6},{"shortcodes":["jar"],"annotation":"jar","tags":["condiment","container","empty","sauce","store"],"emoji":"🫙","order":3779,"group":4,"version":14},{"shortcodes":["amphora"],"annotation":"amphora","tags":["aquarius","cooking","drink","jug","zodiac"],"emoji":"🏺","order":3780,"group":4,"version":1},{"shortcodes":["earth_africa","earth_europe"],"annotation":"globe showing Europe-Africa","tags":["africa","earth","europe","globe","globe showing europe-africa","world"],"emoji":"🌍️","order":3781,"group":5,"version":0.7},{"shortcodes":["earth_americas"],"annotation":"globe showing Americas","tags":["americas","earth","globe","globe showing americas","world"],"emoji":"🌎️","order":3782,"group":5,"version":0.7},{"shortcodes":["earth_asia"],"annotation":"globe showing Asia-Australia","tags":["asia","australia","earth","globe","globe showing asia-australia","world"],"emoji":"🌏️","order":3783,"group":5,"version":0.6},{"shortcodes":["globe_with_meridians"],"annotation":"globe with meridians","tags":["earth","globe","meridians","world"],"emoji":"🌐","order":3784,"group":5,"version":1},{"shortcodes":["world_map"],"annotation":"world map","tags":["map","world"],"emoji":"🗺️","order":3786,"group":5,"version":0.7},{"shortcodes":["japan_map"],"annotation":"map of Japan","tags":["japan","map","map of japan"],"emoji":"🗾","order":3787,"group":5,"version":0.6},{"shortcodes":["compass"],"annotation":"compass","tags":["magnetic","navigation","orienteering"],"emoji":"🧭","order":3788,"group":5,"version":11},{"shortcodes":["mountain_snow"],"annotation":"snow-capped mountain","tags":["cold","mountain","snow"],"emoji":"🏔️","order":3790,"group":5,"version":0.7},{"shortcodes":["mountain"],"annotation":"mountain","tags":["mountain"],"emoji":"⛰️","order":3792,"group":5,"version":0.7},{"shortcodes":["volcano"],"annotation":"volcano","tags":["eruption","mountain"],"emoji":"🌋","order":3793,"group":5,"version":0.6},{"shortcodes":["mount_fuji"],"annotation":"mount fuji","tags":["fuji","mountain"],"emoji":"🗻","order":3794,"group":5,"version":0.6},{"shortcodes":["camping"],"annotation":"camping","tags":["camping"],"emoji":"🏕️","order":3796,"group":5,"version":0.7},{"shortcodes":["beach","beach_with_umbrella"],"annotation":"beach with umbrella","tags":["beach","umbrella"],"emoji":"🏖️","order":3798,"group":5,"version":0.7},{"shortcodes":["desert"],"annotation":"desert","tags":["desert"],"emoji":"🏜️","order":3800,"group":5,"version":0.7},{"shortcodes":["desert_island","island"],"annotation":"desert island","tags":["desert","island"],"emoji":"🏝️","order":3802,"group":5,"version":0.7},{"shortcodes":["national_park"],"annotation":"national park","tags":["park"],"emoji":"🏞️","order":3804,"group":5,"version":0.7},{"shortcodes":["stadium"],"annotation":"stadium","tags":["stadium"],"emoji":"🏟️","order":3806,"group":5,"version":0.7},{"shortcodes":["classical_building"],"annotation":"classical building","tags":["classical"],"emoji":"🏛️","order":3808,"group":5,"version":0.7},{"shortcodes":["building_construction","construction_site"],"annotation":"building construction","tags":["construction"],"emoji":"🏗️","order":3810,"group":5,"version":0.7},{"shortcodes":["bricks"],"annotation":"brick","tags":["bricks","clay","mortar","wall"],"emoji":"🧱","order":3811,"group":5,"version":11},{"shortcodes":["rock"],"annotation":"rock","tags":["boulder","heavy","solid","stone"],"emoji":"🪨","order":3812,"group":5,"version":13},{"shortcodes":["wood"],"annotation":"wood","tags":["log","lumber","timber"],"emoji":"🪵","order":3813,"group":5,"version":13},{"shortcodes":["hut"],"annotation":"hut","tags":["house","roundhouse","yurt"],"emoji":"🛖","order":3814,"group":5,"version":13},{"shortcodes":["homes","houses"],"annotation":"houses","tags":["houses"],"emoji":"🏘️","order":3816,"group":5,"version":0.7},{"shortcodes":["derelict_house","house_abandoned"],"annotation":"derelict house","tags":["derelict","house"],"emoji":"🏚️","order":3818,"group":5,"version":0.7},{"shortcodes":["house"],"annotation":"house","tags":["home"],"emoji":"🏠️","order":3819,"group":5,"version":0.6},{"shortcodes":["house_with_garden"],"annotation":"house with garden","tags":["garden","home","house"],"emoji":"🏡","order":3820,"group":5,"version":0.6},{"shortcodes":["office"],"annotation":"office building","tags":["building"],"emoji":"🏢","order":3821,"group":5,"version":0.6},{"shortcodes":["post_office"],"annotation":"Japanese post office","tags":["japanese","japanese post office","post"],"emoji":"🏣","order":3822,"group":5,"version":0.6},{"shortcodes":["european_post_office"],"annotation":"post office","tags":["european","post"],"emoji":"🏤","order":3823,"group":5,"version":1},{"shortcodes":["hospital"],"annotation":"hospital","tags":["doctor","medicine"],"emoji":"🏥","order":3824,"group":5,"version":0.6},{"shortcodes":["bank"],"annotation":"bank","tags":["building"],"emoji":"🏦","order":3825,"group":5,"version":0.6},{"shortcodes":["hotel"],"annotation":"hotel","tags":["building"],"emoji":"🏨","order":3826,"group":5,"version":0.6},{"shortcodes":["love_hotel"],"annotation":"love hotel","tags":["hotel","love"],"emoji":"🏩","order":3827,"group":5,"version":0.6},{"shortcodes":["convenience_store"],"annotation":"convenience store","tags":["convenience","store"],"emoji":"🏪","order":3828,"group":5,"version":0.6},{"shortcodes":["school"],"annotation":"school","tags":["building"],"emoji":"🏫","order":3829,"group":5,"version":0.6},{"shortcodes":["department_store"],"annotation":"department store","tags":["department","store"],"emoji":"🏬","order":3830,"group":5,"version":0.6},{"shortcodes":["factory"],"annotation":"factory","tags":["building"],"emoji":"🏭️","order":3831,"group":5,"version":0.6},{"shortcodes":["japanese_castle"],"annotation":"Japanese castle","tags":["castle","japanese"],"emoji":"🏯","order":3832,"group":5,"version":0.6},{"shortcodes":["castle","european_castle"],"annotation":"castle","tags":["european"],"emoji":"🏰","order":3833,"group":5,"version":0.6},{"shortcodes":["wedding"],"annotation":"wedding","tags":["chapel","romance"],"emoji":"💒","order":3834,"group":5,"version":0.6},{"shortcodes":["tokyo_tower"],"annotation":"Tokyo tower","tags":["tokyo","tower"],"emoji":"🗼","order":3835,"group":5,"version":0.6},{"shortcodes":["statue_of_liberty"],"annotation":"Statue of Liberty","tags":["liberty","statue","statue of liberty"],"emoji":"🗽","order":3836,"group":5,"version":0.6},{"shortcodes":["church"],"annotation":"church","tags":["christian","cross","religion"],"emoji":"⛪️","order":3837,"group":5,"version":0.6},{"shortcodes":["mosque"],"annotation":"mosque","tags":["islam","muslim","religion"],"emoji":"🕌","order":3838,"group":5,"version":1},{"shortcodes":["hindu_temple"],"annotation":"hindu temple","tags":["hindu","temple"],"emoji":"🛕","order":3839,"group":5,"version":12},{"shortcodes":["synagogue"],"annotation":"synagogue","tags":["jew","jewish","religion","temple"],"emoji":"🕍","order":3840,"group":5,"version":1},{"shortcodes":["shinto_shrine"],"annotation":"shinto shrine","tags":["religion","shinto","shrine"],"emoji":"⛩️","order":3842,"group":5,"version":0.7},{"shortcodes":["kaaba"],"annotation":"kaaba","tags":["islam","muslim","religion"],"emoji":"🕋","order":3843,"group":5,"version":1},{"shortcodes":["fountain"],"annotation":"fountain","tags":["fountain"],"emoji":"⛲️","order":3844,"group":5,"version":0.6},{"shortcodes":["tent"],"annotation":"tent","tags":["camping"],"emoji":"⛺️","order":3845,"group":5,"version":0.6},{"shortcodes":["foggy"],"annotation":"foggy","tags":["fog"],"emoji":"🌁","order":3846,"group":5,"version":0.6},{"shortcodes":["night_with_stars"],"annotation":"night with stars","tags":["night","star"],"emoji":"🌃","order":3847,"group":5,"version":0.6},{"shortcodes":["cityscape"],"annotation":"cityscape","tags":["city"],"emoji":"🏙️","order":3849,"group":5,"version":0.7},{"shortcodes":["sunrise_over_mountains"],"annotation":"sunrise over mountains","tags":["morning","mountain","sun","sunrise"],"emoji":"🌄","order":3850,"group":5,"version":0.6},{"shortcodes":["sunrise"],"annotation":"sunrise","tags":["morning","sun"],"emoji":"🌅","order":3851,"group":5,"version":0.6},{"shortcodes":["city_dusk"],"annotation":"cityscape at dusk","tags":["city","dusk","evening","landscape","sunset"],"emoji":"🌆","order":3852,"group":5,"version":0.6},{"shortcodes":["city_sunrise","city_sunset"],"annotation":"sunset","tags":["dusk","sun"],"emoji":"🌇","order":3853,"group":5,"version":0.6},{"shortcodes":["bridge_at_night"],"annotation":"bridge at night","tags":["bridge","night"],"emoji":"🌉","order":3854,"group":5,"version":0.6},{"shortcodes":["hotsprings"],"annotation":"hot springs","tags":["hot","hotsprings","springs","steaming"],"emoji":"♨️","order":3856,"group":5,"version":0.6},{"shortcodes":["carousel_horse"],"annotation":"carousel horse","tags":["carousel","horse"],"emoji":"🎠","order":3857,"group":5,"version":0.6},{"shortcodes":["playground_slide","slide"],"annotation":"playground slide","tags":["amusement park","play","theme park"],"emoji":"🛝","order":3858,"group":5,"version":14},{"shortcodes":["ferris_wheel"],"annotation":"ferris wheel","tags":["amusement park","ferris","theme park","wheel"],"emoji":"🎡","order":3859,"group":5,"version":0.6},{"shortcodes":["roller_coaster"],"annotation":"roller coaster","tags":["amusement park","coaster","roller","theme park"],"emoji":"🎢","order":3860,"group":5,"version":0.6},{"shortcodes":["barber","barber_pole"],"annotation":"barber pole","tags":["barber","haircut","pole"],"emoji":"💈","order":3861,"group":5,"version":0.6},{"shortcodes":["circus_tent"],"annotation":"circus tent","tags":["circus","tent"],"emoji":"🎪","order":3862,"group":5,"version":0.6},{"shortcodes":["steam_locomotive"],"annotation":"locomotive","tags":["engine","railway","steam","train"],"emoji":"🚂","order":3863,"group":5,"version":1},{"shortcodes":["railway_car"],"annotation":"railway car","tags":["car","electric","railway","train","tram","trolleybus"],"emoji":"🚃","order":3864,"group":5,"version":0.6},{"shortcodes":["bullettrain_side"],"annotation":"high-speed train","tags":["railway","shinkansen","speed","train"],"emoji":"🚄","order":3865,"group":5,"version":0.6},{"shortcodes":["bullettrain_front"],"annotation":"bullet train","tags":["bullet","railway","shinkansen","speed","train"],"emoji":"🚅","order":3866,"group":5,"version":0.6},{"shortcodes":["train"],"annotation":"train","tags":["railway"],"emoji":"🚆","order":3867,"group":5,"version":1},{"shortcodes":["metro"],"annotation":"metro","tags":["subway"],"emoji":"🚇️","order":3868,"group":5,"version":0.6},{"shortcodes":["light_rail"],"annotation":"light rail","tags":["railway"],"emoji":"🚈","order":3869,"group":5,"version":1},{"shortcodes":["station"],"annotation":"station","tags":["railway","train"],"emoji":"🚉","order":3870,"group":5,"version":0.6},{"shortcodes":["tram"],"annotation":"tram","tags":["trolleybus"],"emoji":"🚊","order":3871,"group":5,"version":1},{"shortcodes":["monorail"],"annotation":"monorail","tags":["vehicle"],"emoji":"🚝","order":3872,"group":5,"version":1},{"shortcodes":["mountain_railway"],"annotation":"mountain railway","tags":["car","mountain","railway"],"emoji":"🚞","order":3873,"group":5,"version":1},{"shortcodes":["tram_car"],"annotation":"tram car","tags":["car","tram","trolleybus"],"emoji":"🚋","order":3874,"group":5,"version":1},{"shortcodes":["bus"],"annotation":"bus","tags":["vehicle"],"emoji":"🚌","order":3875,"group":5,"version":0.6},{"shortcodes":["oncoming_bus"],"annotation":"oncoming bus","tags":["bus","oncoming"],"emoji":"🚍️","order":3876,"group":5,"version":0.7},{"shortcodes":["trolleybus"],"annotation":"trolleybus","tags":["bus","tram","trolley"],"emoji":"🚎","order":3877,"group":5,"version":1},{"shortcodes":["minibus"],"annotation":"minibus","tags":["bus"],"emoji":"🚐","order":3878,"group":5,"version":1},{"shortcodes":["ambulance"],"annotation":"ambulance","tags":["vehicle"],"emoji":"🚑️","order":3879,"group":5,"version":0.6},{"shortcodes":["fire_engine"],"annotation":"fire engine","tags":["engine","fire","truck"],"emoji":"🚒","order":3880,"group":5,"version":0.6},{"shortcodes":["police_car"],"annotation":"police car","tags":["car","patrol","police"],"emoji":"🚓","order":3881,"group":5,"version":0.6},{"shortcodes":["oncoming_police_car"],"annotation":"oncoming police car","tags":["car","oncoming","police"],"emoji":"🚔️","order":3882,"group":5,"version":0.7},{"shortcodes":["taxi"],"annotation":"taxi","tags":["vehicle"],"emoji":"🚕","order":3883,"group":5,"version":0.6},{"shortcodes":["oncoming_taxi"],"annotation":"oncoming taxi","tags":["oncoming","taxi"],"emoji":"🚖","order":3884,"group":5,"version":1},{"shortcodes":["car","red_car"],"annotation":"automobile","tags":["car"],"emoji":"🚗","order":3885,"group":5,"version":0.6},{"shortcodes":["oncoming_automobile"],"annotation":"oncoming automobile","tags":["automobile","car","oncoming"],"emoji":"🚘️","order":3886,"group":5,"version":0.7},{"shortcodes":["blue_car","suv"],"annotation":"sport utility vehicle","tags":["recreational","sport utility"],"emoji":"🚙","order":3887,"group":5,"version":0.6},{"shortcodes":["pickup_truck"],"annotation":"pickup truck","tags":["pick-up","pickup","truck"],"emoji":"🛻","order":3888,"group":5,"version":13},{"shortcodes":["delivery_truck","truck"],"annotation":"delivery truck","tags":["delivery","truck"],"emoji":"🚚","order":3889,"group":5,"version":0.6},{"shortcodes":["articulated_lorry"],"annotation":"articulated lorry","tags":["lorry","semi","truck"],"emoji":"🚛","order":3890,"group":5,"version":1},{"shortcodes":["tractor"],"annotation":"tractor","tags":["vehicle"],"emoji":"🚜","order":3891,"group":5,"version":1},{"shortcodes":["racing_car"],"annotation":"racing car","tags":["car","racing"],"emoji":"🏎️","order":3893,"group":5,"version":0.7},{"shortcodes":["motorcycle"],"annotation":"motorcycle","tags":["racing"],"emoji":"🏍️","order":3895,"group":5,"version":0.7},{"shortcodes":["motor_scooter"],"annotation":"motor scooter","tags":["motor","scooter"],"emoji":"🛵","order":3896,"group":5,"version":3},{"shortcodes":["manual_wheelchair"],"annotation":"manual wheelchair","tags":["accessibility"],"emoji":"🦽","order":3897,"group":5,"version":12},{"shortcodes":["motorized_wheelchair"],"annotation":"motorized wheelchair","tags":["accessibility"],"emoji":"🦼","order":3898,"group":5,"version":12},{"shortcodes":["auto_rickshaw"],"annotation":"auto rickshaw","tags":["tuk tuk"],"emoji":"🛺","order":3899,"group":5,"version":12},{"shortcodes":["bicycle","bike"],"annotation":"bicycle","tags":["bike"],"emoji":"🚲️","order":3900,"group":5,"version":0.6},{"shortcodes":["scooter"],"annotation":"kick scooter","tags":["kick","scooter"],"emoji":"🛴","order":3901,"group":5,"version":3},{"shortcodes":["skateboard"],"annotation":"skateboard","tags":["board"],"emoji":"🛹","order":3902,"group":5,"version":11},{"shortcodes":["roller_skate"],"annotation":"roller skate","tags":["roller","skate"],"emoji":"🛼","order":3903,"group":5,"version":13},{"shortcodes":["busstop"],"annotation":"bus stop","tags":["bus","stop"],"emoji":"🚏","order":3904,"group":5,"version":0.6},{"shortcodes":["motorway"],"annotation":"motorway","tags":["highway","road"],"emoji":"🛣️","order":3906,"group":5,"version":0.7},{"shortcodes":["railway_track"],"annotation":"railway track","tags":["railway","train"],"emoji":"🛤️","order":3908,"group":5,"version":0.7},{"shortcodes":["oil_drum"],"annotation":"oil drum","tags":["drum","oil"],"emoji":"🛢️","order":3910,"group":5,"version":0.7},{"shortcodes":["fuelpump"],"annotation":"fuel pump","tags":["diesel","fuel","fuelpump","gas","pump","station"],"emoji":"⛽️","order":3911,"group":5,"version":0.6},{"shortcodes":["wheel"],"annotation":"wheel","tags":["circle","tire","turn"],"emoji":"🛞","order":3912,"group":5,"version":14},{"shortcodes":["rotating_light"],"annotation":"police car light","tags":["beacon","car","light","police","revolving"],"emoji":"🚨","order":3913,"group":5,"version":0.6},{"shortcodes":["traffic_light"],"annotation":"horizontal traffic light","tags":["light","signal","traffic"],"emoji":"🚥","order":3914,"group":5,"version":0.6},{"shortcodes":["vertical_traffic_light"],"annotation":"vertical traffic light","tags":["light","signal","traffic"],"emoji":"🚦","order":3915,"group":5,"version":1},{"shortcodes":["octagonal_sign","stop_sign"],"annotation":"stop sign","tags":["octagonal","sign","stop"],"emoji":"🛑","order":3916,"group":5,"version":3},{"shortcodes":["construction"],"annotation":"construction","tags":["barrier"],"emoji":"🚧","order":3917,"group":5,"version":0.6},{"shortcodes":["anchor"],"annotation":"anchor","tags":["ship","tool"],"emoji":"⚓️","order":3918,"group":5,"version":0.6},{"shortcodes":["lifebuoy","ring_buoy"],"annotation":"ring buoy","tags":["float","life preserver","life saver","rescue","safety"],"emoji":"🛟","order":3919,"group":5,"version":14},{"shortcodes":["sailboat"],"annotation":"sailboat","tags":["boat","resort","sea","yacht"],"emoji":"⛵️","order":3920,"group":5,"version":0.6},{"shortcodes":["canoe"],"annotation":"canoe","tags":["boat"],"emoji":"🛶","order":3921,"group":5,"version":3},{"shortcodes":["speedboat"],"annotation":"speedboat","tags":["boat"],"emoji":"🚤","order":3922,"group":5,"version":0.6},{"shortcodes":["cruise_ship","passenger_ship"],"annotation":"passenger ship","tags":["passenger","ship"],"emoji":"🛳️","order":3924,"group":5,"version":0.7},{"shortcodes":["ferry"],"annotation":"ferry","tags":["boat","passenger"],"emoji":"⛴️","order":3926,"group":5,"version":0.7},{"shortcodes":["motorboat"],"annotation":"motor boat","tags":["boat","motorboat"],"emoji":"🛥️","order":3928,"group":5,"version":0.7},{"shortcodes":["ship"],"annotation":"ship","tags":["boat","passenger"],"emoji":"🚢","order":3929,"group":5,"version":0.6},{"shortcodes":["airplane"],"annotation":"airplane","tags":["aeroplane"],"emoji":"✈️","order":3931,"group":5,"version":0.6},{"shortcodes":["small_airplane"],"annotation":"small airplane","tags":["aeroplane","airplane"],"emoji":"🛩️","order":3933,"group":5,"version":0.7},{"shortcodes":["airplane_departure"],"annotation":"airplane departure","tags":["aeroplane","airplane","check-in","departure","departures"],"emoji":"🛫","order":3934,"group":5,"version":1},{"shortcodes":["airplane_arriving"],"annotation":"airplane arrival","tags":["aeroplane","airplane","arrivals","arriving","landing"],"emoji":"🛬","order":3935,"group":5,"version":1},{"shortcodes":["parachute"],"annotation":"parachute","tags":["hang-glide","parasail","skydive"],"emoji":"🪂","order":3936,"group":5,"version":12},{"shortcodes":["seat"],"annotation":"seat","tags":["chair"],"emoji":"💺","order":3937,"group":5,"version":0.6},{"shortcodes":["helicopter"],"annotation":"helicopter","tags":["vehicle"],"emoji":"🚁","order":3938,"group":5,"version":1},{"shortcodes":["suspension_railway"],"annotation":"suspension railway","tags":["railway","suspension"],"emoji":"🚟","order":3939,"group":5,"version":1},{"shortcodes":["mountain_cableway"],"annotation":"mountain cableway","tags":["cable","gondola","mountain"],"emoji":"🚠","order":3940,"group":5,"version":1},{"shortcodes":["aerial_tramway"],"annotation":"aerial tramway","tags":["aerial","cable","car","gondola","tramway"],"emoji":"🚡","order":3941,"group":5,"version":1},{"shortcodes":["satellite"],"annotation":"satellite","tags":["space"],"emoji":"🛰️","order":3943,"group":5,"version":0.7},{"shortcodes":["rocket"],"annotation":"rocket","tags":["space"],"emoji":"🚀","order":3944,"group":5,"version":0.6},{"shortcodes":["flying_saucer"],"annotation":"flying saucer","tags":["ufo"],"emoji":"🛸","order":3945,"group":5,"version":5},{"shortcodes":["bellhop"],"annotation":"bellhop bell","tags":["bell","bellhop","hotel"],"emoji":"🛎️","order":3947,"group":5,"version":0.7},{"shortcodes":["luggage"],"annotation":"luggage","tags":["packing","travel"],"emoji":"🧳","order":3948,"group":5,"version":11},{"shortcodes":["hourglass"],"annotation":"hourglass done","tags":["sand","timer"],"emoji":"⌛️","order":3949,"group":5,"version":0.6},{"shortcodes":["hourglass_flowing_sand"],"annotation":"hourglass not done","tags":["hourglass","sand","timer"],"emoji":"⏳️","order":3950,"group":5,"version":0.6},{"shortcodes":["watch"],"annotation":"watch","tags":["clock"],"emoji":"⌚️","order":3951,"group":5,"version":0.6},{"shortcodes":["alarm_clock"],"annotation":"alarm clock","tags":["alarm","clock"],"emoji":"⏰️","order":3952,"group":5,"version":0.6},{"shortcodes":["stopwatch"],"annotation":"stopwatch","tags":["clock"],"emoji":"⏱️","order":3954,"group":5,"version":1},{"shortcodes":["timer_clock"],"annotation":"timer clock","tags":["clock","timer"],"emoji":"⏲️","order":3956,"group":5,"version":1},{"shortcodes":["clock"],"annotation":"mantelpiece clock","tags":["clock"],"emoji":"🕰️","order":3958,"group":5,"version":0.7},{"shortcodes":["clock12"],"annotation":"twelve o’clock","tags":["00","12","12:00","clock","o’clock","twelve"],"emoji":"🕛️","order":3959,"group":5,"version":0.6},{"shortcodes":["clock1230"],"annotation":"twelve-thirty","tags":["12","12:30","clock","thirty","twelve"],"emoji":"🕧️","order":3960,"group":5,"version":0.7},{"shortcodes":["clock1"],"annotation":"one o’clock","tags":["00","1","1:00","clock","one","o’clock"],"emoji":"🕐️","order":3961,"group":5,"version":0.6},{"shortcodes":["clock130"],"annotation":"one-thirty","tags":["1","1:30","clock","one","thirty"],"emoji":"🕜️","order":3962,"group":5,"version":0.7},{"shortcodes":["clock2"],"annotation":"two o’clock","tags":["00","2","2:00","clock","o’clock","two"],"emoji":"🕑️","order":3963,"group":5,"version":0.6},{"shortcodes":["clock230"],"annotation":"two-thirty","tags":["2","2:30","clock","thirty","two"],"emoji":"🕝️","order":3964,"group":5,"version":0.7},{"shortcodes":["clock3"],"annotation":"three o’clock","tags":["00","3","3:00","clock","o’clock","three"],"emoji":"🕒️","order":3965,"group":5,"version":0.6},{"shortcodes":["clock330"],"annotation":"three-thirty","tags":["3","3:30","clock","thirty","three"],"emoji":"🕞️","order":3966,"group":5,"version":0.7},{"shortcodes":["clock4"],"annotation":"four o’clock","tags":["00","4","4:00","clock","four","o’clock"],"emoji":"🕓️","order":3967,"group":5,"version":0.6},{"shortcodes":["clock430"],"annotation":"four-thirty","tags":["4","4:30","clock","four","thirty"],"emoji":"🕟️","order":3968,"group":5,"version":0.7},{"shortcodes":["clock5"],"annotation":"five o’clock","tags":["00","5","5:00","clock","five","o’clock"],"emoji":"🕔️","order":3969,"group":5,"version":0.6},{"shortcodes":["clock530"],"annotation":"five-thirty","tags":["5","5:30","clock","five","thirty"],"emoji":"🕠️","order":3970,"group":5,"version":0.7},{"shortcodes":["clock6"],"annotation":"six o’clock","tags":["00","6","6:00","clock","o’clock","six"],"emoji":"🕕️","order":3971,"group":5,"version":0.6},{"shortcodes":["clock630"],"annotation":"six-thirty","tags":["6","6:30","clock","six","thirty"],"emoji":"🕡️","order":3972,"group":5,"version":0.7},{"shortcodes":["clock7"],"annotation":"seven o’clock","tags":["00","7","7:00","clock","o’clock","seven"],"emoji":"🕖️","order":3973,"group":5,"version":0.6},{"shortcodes":["clock730"],"annotation":"seven-thirty","tags":["7","7:30","clock","seven","thirty"],"emoji":"🕢️","order":3974,"group":5,"version":0.7},{"shortcodes":["clock8"],"annotation":"eight o’clock","tags":["00","8","8:00","clock","eight","o’clock"],"emoji":"🕗️","order":3975,"group":5,"version":0.6},{"shortcodes":["clock830"],"annotation":"eight-thirty","tags":["8","8:30","clock","eight","thirty"],"emoji":"🕣️","order":3976,"group":5,"version":0.7},{"shortcodes":["clock9"],"annotation":"nine o’clock","tags":["00","9","9:00","clock","nine","o’clock"],"emoji":"🕘️","order":3977,"group":5,"version":0.6},{"shortcodes":["clock930"],"annotation":"nine-thirty","tags":["9","9:30","clock","nine","thirty"],"emoji":"🕤️","order":3978,"group":5,"version":0.7},{"shortcodes":["clock10"],"annotation":"ten o’clock","tags":["00","10","10:00","clock","o’clock","ten"],"emoji":"🕙️","order":3979,"group":5,"version":0.6},{"shortcodes":["clock1030"],"annotation":"ten-thirty","tags":["10","10:30","clock","ten","thirty"],"emoji":"🕥️","order":3980,"group":5,"version":0.7},{"shortcodes":["clock11"],"annotation":"eleven o’clock","tags":["00","11","11:00","clock","eleven","o’clock"],"emoji":"🕚️","order":3981,"group":5,"version":0.6},{"shortcodes":["clock1130"],"annotation":"eleven-thirty","tags":["11","11:30","clock","eleven","thirty"],"emoji":"🕦️","order":3982,"group":5,"version":0.7},{"shortcodes":["new_moon"],"annotation":"new moon","tags":["dark","moon"],"emoji":"🌑","order":3983,"group":5,"version":0.6},{"shortcodes":["waxing_crescent_moon"],"annotation":"waxing crescent moon","tags":["crescent","moon","waxing"],"emoji":"🌒","order":3984,"group":5,"version":1},{"shortcodes":["first_quarter_moon"],"annotation":"first quarter moon","tags":["moon","quarter"],"emoji":"🌓","order":3985,"group":5,"version":0.6},{"shortcodes":["waxing_gibbous_moon"],"annotation":"waxing gibbous moon","tags":["gibbous","moon","waxing"],"emoji":"🌔","order":3986,"group":5,"version":0.6},{"shortcodes":["full_moon"],"annotation":"full moon","tags":["full","moon"],"emoji":"🌕️","order":3987,"group":5,"version":0.6},{"shortcodes":["waning_gibbous_moon"],"annotation":"waning gibbous moon","tags":["gibbous","moon","waning"],"emoji":"🌖","order":3988,"group":5,"version":1},{"shortcodes":["last_quarter_moon"],"annotation":"last quarter moon","tags":["moon","quarter"],"emoji":"🌗","order":3989,"group":5,"version":1},{"shortcodes":["waning_crescent_moon"],"annotation":"waning crescent moon","tags":["crescent","moon","waning"],"emoji":"🌘","order":3990,"group":5,"version":1},{"shortcodes":["crescent_moon"],"annotation":"crescent moon","tags":["crescent","moon"],"emoji":"🌙","order":3991,"group":5,"version":0.6},{"shortcodes":["new_moon_with_face"],"annotation":"new moon face","tags":["face","moon"],"emoji":"🌚","order":3992,"group":5,"version":1},{"shortcodes":["first_quarter_moon_with_face"],"annotation":"first quarter moon face","tags":["face","moon","quarter"],"emoji":"🌛","order":3993,"group":5,"version":0.6},{"shortcodes":["last_quarter_moon_with_face"],"annotation":"last quarter moon face","tags":["face","moon","quarter"],"emoji":"🌜️","order":3994,"group":5,"version":0.7},{"shortcodes":["thermometer"],"annotation":"thermometer","tags":["weather"],"emoji":"🌡️","order":3996,"group":5,"version":0.7},{"shortcodes":["sun"],"annotation":"sun","tags":["bright","rays","sunny"],"emoji":"☀️","order":3998,"group":5,"version":0.6},{"shortcodes":["full_moon_with_face"],"annotation":"full moon face","tags":["bright","face","full","moon"],"emoji":"🌝","order":3999,"group":5,"version":1},{"shortcodes":["sun_with_face"],"annotation":"sun with face","tags":["bright","face","sun"],"emoji":"🌞","order":4000,"group":5,"version":1},{"shortcodes":["ringed_planet","saturn"],"annotation":"ringed planet","tags":["saturn","saturnine"],"emoji":"🪐","order":4001,"group":5,"version":12},{"shortcodes":["star"],"annotation":"star","tags":["star"],"emoji":"⭐️","order":4002,"group":5,"version":0.6},{"shortcodes":["glowing_star","star2"],"annotation":"glowing star","tags":["glittery","glow","shining","sparkle","star"],"emoji":"🌟","order":4003,"group":5,"version":0.6},{"shortcodes":["shooting_star","stars"],"annotation":"shooting star","tags":["falling","shooting","star"],"emoji":"🌠","order":4004,"group":5,"version":0.6},{"shortcodes":["milky_way"],"annotation":"milky way","tags":["space"],"emoji":"🌌","order":4005,"group":5,"version":0.6},{"shortcodes":["cloud"],"annotation":"cloud","tags":["weather"],"emoji":"☁️","order":4007,"group":5,"version":0.6},{"shortcodes":["partly_sunny","sun_behind_cloud"],"annotation":"sun behind cloud","tags":["cloud","sun"],"emoji":"⛅️","order":4008,"group":5,"version":0.6},{"shortcodes":["stormy","thunder_cloud_and_rain"],"annotation":"cloud with lightning and rain","tags":["cloud","rain","thunder"],"emoji":"⛈️","order":4010,"group":5,"version":0.7},{"shortcodes":["sun_behind_small_cloud","sunny"],"annotation":"sun behind small cloud","tags":["cloud","sun"],"emoji":"🌤️","order":4012,"group":5,"version":0.7},{"shortcodes":["cloudy","sun_behind_large_cloud"],"annotation":"sun behind large cloud","tags":["cloud","sun"],"emoji":"🌥️","order":4014,"group":5,"version":0.7},{"shortcodes":["sun_and_rain","sun_behind_rain_cloud"],"annotation":"sun behind rain cloud","tags":["cloud","rain","sun"],"emoji":"🌦️","order":4016,"group":5,"version":0.7},{"shortcodes":["cloud_with_rain","rainy"],"annotation":"cloud with rain","tags":["cloud","rain"],"emoji":"🌧️","order":4018,"group":5,"version":0.7},{"shortcodes":["cloud_with_snow","snowy"],"annotation":"cloud with snow","tags":["cloud","cold","snow"],"emoji":"🌨️","order":4020,"group":5,"version":0.7},{"shortcodes":["cloud_with_lightning","lightning"],"annotation":"cloud with lightning","tags":["cloud","lightning"],"emoji":"🌩️","order":4022,"group":5,"version":0.7},{"shortcodes":["tornado"],"annotation":"tornado","tags":["cloud","whirlwind"],"emoji":"🌪️","order":4024,"group":5,"version":0.7},{"shortcodes":["fog"],"annotation":"fog","tags":["cloud"],"emoji":"🌫️","order":4026,"group":5,"version":0.7},{"shortcodes":["wind_blowing_face"],"annotation":"wind face","tags":["blow","cloud","face","wind"],"emoji":"🌬️","order":4028,"group":5,"version":0.7},{"shortcodes":["cyclone"],"annotation":"cyclone","tags":["dizzy","hurricane","twister","typhoon"],"emoji":"🌀","order":4029,"group":5,"version":0.6},{"shortcodes":["rainbow"],"annotation":"rainbow","tags":["rain"],"emoji":"🌈","order":4030,"group":5,"version":0.6},{"shortcodes":["closed_umbrella"],"annotation":"closed umbrella","tags":["clothing","rain","umbrella"],"emoji":"🌂","order":4031,"group":5,"version":0.6},{"shortcodes":["umbrella"],"annotation":"umbrella","tags":["clothing","rain"],"emoji":"☂️","order":4033,"group":5,"version":0.7},{"shortcodes":["umbrella_with_rain"],"annotation":"umbrella with rain drops","tags":["clothing","drop","rain","umbrella"],"emoji":"☔️","order":4034,"group":5,"version":0.6},{"shortcodes":["beach_umbrella","umbrella_on_ground"],"annotation":"umbrella on ground","tags":["rain","sun","umbrella"],"emoji":"⛱️","order":4036,"group":5,"version":0.7},{"shortcodes":["high_voltage","zap"],"annotation":"high voltage","tags":["danger","electric","lightning","voltage","zap"],"emoji":"⚡️","order":4037,"group":5,"version":0.6},{"shortcodes":["snowflake"],"annotation":"snowflake","tags":["cold","snow"],"emoji":"❄️","order":4039,"group":5,"version":0.6},{"shortcodes":["snowman2"],"annotation":"snowman","tags":["cold","snow"],"emoji":"☃️","order":4041,"group":5,"version":0.7},{"shortcodes":["snowman"],"annotation":"snowman without snow","tags":["cold","snow","snowman"],"emoji":"⛄️","order":4042,"group":5,"version":0.6},{"shortcodes":["comet"],"annotation":"comet","tags":["space"],"emoji":"☄️","order":4044,"group":5,"version":1},{"shortcodes":["fire"],"annotation":"fire","tags":["flame","tool"],"emoji":"🔥","order":4045,"group":5,"version":0.6},{"shortcodes":["droplet"],"annotation":"droplet","tags":["cold","comic","drop","sweat"],"emoji":"💧","order":4046,"group":5,"version":0.6},{"shortcodes":["ocean","water_wave"],"annotation":"water wave","tags":["ocean","water","wave"],"emoji":"🌊","order":4047,"group":5,"version":0.6},{"shortcodes":["jack_o_lantern"],"annotation":"jack-o-lantern","tags":["celebration","halloween","jack","lantern"],"emoji":"🎃","order":4048,"group":6,"version":0.6},{"shortcodes":["christmas_tree"],"annotation":"Christmas tree","tags":["celebration","christmas","tree"],"emoji":"🎄","order":4049,"group":6,"version":0.6},{"shortcodes":["fireworks"],"annotation":"fireworks","tags":["celebration"],"emoji":"🎆","order":4050,"group":6,"version":0.6},{"shortcodes":["sparkler"],"annotation":"sparkler","tags":["celebration","fireworks","sparkle"],"emoji":"🎇","order":4051,"group":6,"version":0.6},{"shortcodes":["firecracker"],"annotation":"firecracker","tags":["dynamite","explosive","fireworks"],"emoji":"🧨","order":4052,"group":6,"version":11},{"shortcodes":["sparkles"],"annotation":"sparkles","tags":["*","sparkle","star"],"emoji":"✨️","order":4053,"group":6,"version":0.6},{"shortcodes":["balloon"],"annotation":"balloon","tags":["celebration"],"emoji":"🎈","order":4054,"group":6,"version":0.6},{"shortcodes":["party","party_popper","tada"],"annotation":"party popper","tags":["celebration","party","popper","tada"],"emoji":"🎉","order":4055,"group":6,"version":0.6},{"shortcodes":["confetti_ball"],"annotation":"confetti ball","tags":["ball","celebration","confetti"],"emoji":"🎊","order":4056,"group":6,"version":0.6},{"shortcodes":["tanabata_tree"],"annotation":"tanabata tree","tags":["banner","celebration","japanese","tree"],"emoji":"🎋","order":4057,"group":6,"version":0.6},{"shortcodes":["bamboo"],"annotation":"pine decoration","tags":["bamboo","celebration","japanese","pine"],"emoji":"🎍","order":4058,"group":6,"version":0.6},{"shortcodes":["dolls"],"annotation":"Japanese dolls","tags":["celebration","doll","festival","japanese","japanese dolls"],"emoji":"🎎","order":4059,"group":6,"version":0.6},{"shortcodes":["carp_streamer","flags"],"annotation":"carp streamer","tags":["carp","celebration","streamer"],"emoji":"🎏","order":4060,"group":6,"version":0.6},{"shortcodes":["wind_chime"],"annotation":"wind chime","tags":["bell","celebration","chime","wind"],"emoji":"🎐","order":4061,"group":6,"version":0.6},{"shortcodes":["moon_ceremony","rice_scene"],"annotation":"moon viewing ceremony","tags":["celebration","ceremony","moon"],"emoji":"🎑","order":4062,"group":6,"version":0.6},{"shortcodes":["red_envelope"],"annotation":"red envelope","tags":["gift","good luck","hóngbāo","lai see","money"],"emoji":"🧧","order":4063,"group":6,"version":11},{"shortcodes":["ribbon"],"annotation":"ribbon","tags":["celebration"],"emoji":"🎀","order":4064,"group":6,"version":0.6},{"shortcodes":["gift"],"annotation":"wrapped gift","tags":["box","celebration","gift","present","wrapped"],"emoji":"🎁","order":4065,"group":6,"version":0.6},{"shortcodes":["reminder_ribbon"],"annotation":"reminder ribbon","tags":["celebration","reminder","ribbon"],"emoji":"🎗️","order":4067,"group":6,"version":0.7},{"shortcodes":["admission_tickets","tickets"],"annotation":"admission tickets","tags":["admission","ticket"],"emoji":"🎟️","order":4069,"group":6,"version":0.7},{"shortcodes":["ticket"],"annotation":"ticket","tags":["admission"],"emoji":"🎫","order":4070,"group":6,"version":0.6},{"shortcodes":["military_medal"],"annotation":"military medal","tags":["celebration","medal","military"],"emoji":"🎖️","order":4072,"group":6,"version":0.7},{"shortcodes":["trophy"],"annotation":"trophy","tags":["prize"],"emoji":"🏆️","order":4073,"group":6,"version":0.6},{"shortcodes":["sports_medal"],"annotation":"sports medal","tags":["medal"],"emoji":"🏅","order":4074,"group":6,"version":1},{"shortcodes":["1st","first_place_medal"],"annotation":"1st place medal","tags":["first","gold","medal"],"emoji":"🥇","order":4075,"group":6,"version":3},{"shortcodes":["2nd","second_place_medal"],"annotation":"2nd place medal","tags":["medal","second","silver"],"emoji":"🥈","order":4076,"group":6,"version":3},{"shortcodes":["3rd","third_place_medal"],"annotation":"3rd place medal","tags":["bronze","medal","third"],"emoji":"🥉","order":4077,"group":6,"version":3},{"shortcodes":["soccer"],"annotation":"soccer ball","tags":["ball","football","soccer"],"emoji":"⚽️","order":4078,"group":6,"version":0.6},{"shortcodes":["baseball"],"annotation":"baseball","tags":["ball"],"emoji":"⚾️","order":4079,"group":6,"version":0.6},{"shortcodes":["softball"],"annotation":"softball","tags":["ball","glove","underarm"],"emoji":"🥎","order":4080,"group":6,"version":11},{"shortcodes":["basketball"],"annotation":"basketball","tags":["ball","hoop"],"emoji":"🏀","order":4081,"group":6,"version":0.6},{"shortcodes":["volleyball"],"annotation":"volleyball","tags":["ball","game"],"emoji":"🏐","order":4082,"group":6,"version":1},{"shortcodes":["football"],"annotation":"american football","tags":["american","ball","football"],"emoji":"🏈","order":4083,"group":6,"version":0.6},{"shortcodes":["rugby_football"],"annotation":"rugby football","tags":["ball","football","rugby"],"emoji":"🏉","order":4084,"group":6,"version":1},{"shortcodes":["tennis"],"annotation":"tennis","tags":["ball","racquet"],"emoji":"🎾","order":4085,"group":6,"version":0.6},{"shortcodes":["flying_disc"],"annotation":"flying disc","tags":["ultimate"],"emoji":"🥏","order":4086,"group":6,"version":11},{"shortcodes":["bowling"],"annotation":"bowling","tags":["ball","game"],"emoji":"🎳","order":4087,"group":6,"version":0.6},{"shortcodes":["cricket_game"],"annotation":"cricket game","tags":["ball","bat","game"],"emoji":"🏏","order":4088,"group":6,"version":1},{"shortcodes":["field_hockey"],"annotation":"field hockey","tags":["ball","field","game","hockey","stick"],"emoji":"🏑","order":4089,"group":6,"version":1},{"shortcodes":["hockey"],"annotation":"ice hockey","tags":["game","hockey","ice","puck","stick"],"emoji":"🏒","order":4090,"group":6,"version":1},{"shortcodes":["lacrosse"],"annotation":"lacrosse","tags":["ball","goal","stick"],"emoji":"🥍","order":4091,"group":6,"version":11},{"shortcodes":["ping_pong"],"annotation":"ping pong","tags":["ball","bat","game","paddle","table tennis"],"emoji":"🏓","order":4092,"group":6,"version":1},{"shortcodes":["badminton"],"annotation":"badminton","tags":["birdie","game","racquet","shuttlecock"],"emoji":"🏸","order":4093,"group":6,"version":1},{"shortcodes":["boxing_glove"],"annotation":"boxing glove","tags":["boxing","glove"],"emoji":"🥊","order":4094,"group":6,"version":3},{"shortcodes":["martial_arts_uniform"],"annotation":"martial arts uniform","tags":["judo","karate","martial arts","taekwondo","uniform"],"emoji":"🥋","order":4095,"group":6,"version":3},{"shortcodes":["goal_net"],"annotation":"goal net","tags":["goal","net"],"emoji":"🥅","order":4096,"group":6,"version":3},{"shortcodes":["golf"],"annotation":"flag in hole","tags":["golf","hole"],"emoji":"⛳️","order":4097,"group":6,"version":0.6},{"shortcodes":["ice_skate"],"annotation":"ice skate","tags":["ice","skate"],"emoji":"⛸️","order":4099,"group":6,"version":0.7},{"shortcodes":["fishing_pole","fishing_pole_and_fish"],"annotation":"fishing pole","tags":["fish","pole"],"emoji":"🎣","order":4100,"group":6,"version":0.6},{"shortcodes":["diving_mask"],"annotation":"diving mask","tags":["diving","scuba","snorkeling"],"emoji":"🤿","order":4101,"group":6,"version":12},{"shortcodes":["running_shirt","running_shirt_with_sash"],"annotation":"running shirt","tags":["athletics","running","sash","shirt"],"emoji":"🎽","order":4102,"group":6,"version":0.6},{"shortcodes":["ski"],"annotation":"skis","tags":["ski","snow"],"emoji":"🎿","order":4103,"group":6,"version":0.6},{"shortcodes":["sled"],"annotation":"sled","tags":["sledge","sleigh"],"emoji":"🛷","order":4104,"group":6,"version":5},{"shortcodes":["curling_stone"],"annotation":"curling stone","tags":["game","rock"],"emoji":"🥌","order":4105,"group":6,"version":5},{"shortcodes":["bullseye","dart","direct_hit"],"annotation":"bullseye","tags":["dart","direct hit","game","hit","target"],"emoji":"🎯","order":4106,"group":6,"version":0.6},{"shortcodes":["yo_yo"],"annotation":"yo-yo","tags":["fluctuate","toy"],"emoji":"🪀","order":4107,"group":6,"version":12},{"shortcodes":["kite"],"annotation":"kite","tags":["fly","soar"],"emoji":"🪁","order":4108,"group":6,"version":12},{"shortcodes":["gun","pistol"],"annotation":"water pistol","tags":["gun","handgun","pistol","revolver","tool","water","weapon"],"emoji":"🔫","order":4109,"group":6,"version":0.6},{"shortcodes":["8ball","billiards"],"annotation":"pool 8 ball","tags":["8","ball","billiard","eight","game"],"emoji":"🎱","order":4110,"group":6,"version":0.6},{"shortcodes":["crystal_ball"],"annotation":"crystal ball","tags":["ball","crystal","fairy tale","fantasy","fortune","tool"],"emoji":"🔮","order":4111,"group":6,"version":0.6},{"shortcodes":["magic_wand"],"annotation":"magic wand","tags":["magic","witch","wizard"],"emoji":"🪄","order":4112,"group":6,"version":13},{"shortcodes":["controller","video_game"],"annotation":"video game","tags":["controller","game"],"emoji":"🎮️","order":4113,"group":6,"version":0.6},{"shortcodes":["joystick"],"annotation":"joystick","tags":["game","video game"],"emoji":"🕹️","order":4115,"group":6,"version":0.7},{"shortcodes":["slot_machine"],"annotation":"slot machine","tags":["game","slot"],"emoji":"🎰","order":4116,"group":6,"version":0.6},{"shortcodes":["game_die"],"annotation":"game die","tags":["dice","die","game"],"emoji":"🎲","order":4117,"group":6,"version":0.6},{"shortcodes":["jigsaw","puzzle_piece"],"annotation":"puzzle piece","tags":["clue","interlocking","jigsaw","piece","puzzle"],"emoji":"🧩","order":4118,"group":6,"version":11},{"shortcodes":["teddy_bear"],"annotation":"teddy bear","tags":["plaything","plush","stuffed","toy"],"emoji":"🧸","order":4119,"group":6,"version":11},{"shortcodes":["pinata"],"annotation":"piñata","tags":["celebration","party"],"emoji":"🪅","order":4120,"group":6,"version":13},{"shortcodes":["disco","disco_ball","mirror_ball"],"annotation":"mirror ball","tags":["dance","disco","glitter","party"],"emoji":"🪩","order":4121,"group":6,"version":14},{"shortcodes":["nesting_dolls"],"annotation":"nesting dolls","tags":["doll","nesting","russia"],"emoji":"🪆","order":4122,"group":6,"version":13},{"shortcodes":["spades"],"annotation":"spade suit","tags":["card","game"],"emoji":"♠️","order":4124,"group":6,"version":0.6},{"shortcodes":["hearts"],"annotation":"heart suit","tags":["card","game"],"emoji":"♥️","order":4126,"group":6,"version":0.6},{"shortcodes":["diamonds"],"annotation":"diamond suit","tags":["card","game"],"emoji":"♦️","order":4128,"group":6,"version":0.6},{"shortcodes":["clubs"],"annotation":"club suit","tags":["card","game"],"emoji":"♣️","order":4130,"group":6,"version":0.6},{"shortcodes":["chess_pawn"],"annotation":"chess pawn","tags":["chess","dupe","expendable"],"emoji":"♟️","order":4132,"group":6,"version":11},{"shortcodes":["black_joker"],"annotation":"joker","tags":["card","game","wildcard"],"emoji":"🃏","order":4133,"group":6,"version":0.6},{"shortcodes":["mahjong"],"annotation":"mahjong red dragon","tags":["game","mahjong","red"],"emoji":"🀄️","order":4134,"group":6,"version":0.6},{"shortcodes":["flower_playing_cards"],"annotation":"flower playing cards","tags":["card","flower","game","japanese","playing"],"emoji":"🎴","order":4135,"group":6,"version":0.6},{"shortcodes":["performing_arts"],"annotation":"performing arts","tags":["art","mask","performing","theater","theatre"],"emoji":"🎭️","order":4136,"group":6,"version":0.6},{"shortcodes":["frame_with_picture","framed_picture"],"annotation":"framed picture","tags":["art","frame","museum","painting","picture"],"emoji":"🖼️","order":4138,"group":6,"version":0.7},{"shortcodes":["art","palette"],"annotation":"artist palette","tags":["art","museum","painting","palette"],"emoji":"🎨","order":4139,"group":6,"version":0.6},{"shortcodes":["thread"],"annotation":"thread","tags":["needle","sewing","spool","string"],"emoji":"🧵","order":4140,"group":6,"version":11},{"shortcodes":["sewing_needle"],"annotation":"sewing needle","tags":["embroidery","needle","sewing","stitches","sutures","tailoring"],"emoji":"🪡","order":4141,"group":6,"version":13},{"shortcodes":["yarn"],"annotation":"yarn","tags":["ball","crochet","knit"],"emoji":"🧶","order":4142,"group":6,"version":11},{"shortcodes":["knot"],"annotation":"knot","tags":["rope","tangled","tie","twine","twist"],"emoji":"🪢","order":4143,"group":6,"version":13},{"shortcodes":["eyeglasses","glasses"],"annotation":"glasses","tags":["clothing","eye","eyeglasses","eyewear"],"emoji":"👓️","order":4144,"group":7,"version":0.6},{"shortcodes":["sunglasses"],"annotation":"sunglasses","tags":["dark","eye","eyewear","glasses"],"emoji":"🕶️","order":4146,"group":7,"version":0.7},{"shortcodes":["goggles"],"annotation":"goggles","tags":["eye protection","swimming","welding"],"emoji":"🥽","order":4147,"group":7,"version":11},{"shortcodes":["lab_coat"],"annotation":"lab coat","tags":["doctor","experiment","scientist"],"emoji":"🥼","order":4148,"group":7,"version":11},{"shortcodes":["safety_vest"],"annotation":"safety vest","tags":["emergency","safety","vest"],"emoji":"🦺","order":4149,"group":7,"version":12},{"shortcodes":["necktie"],"annotation":"necktie","tags":["clothing","tie"],"emoji":"👔","order":4150,"group":7,"version":0.6},{"shortcodes":["shirt"],"annotation":"t-shirt","tags":["clothing","shirt","tshirt"],"emoji":"👕","order":4151,"group":7,"version":0.6},{"shortcodes":["jeans"],"annotation":"jeans","tags":["clothing","pants","trousers"],"emoji":"👖","order":4152,"group":7,"version":0.6},{"shortcodes":["scarf"],"annotation":"scarf","tags":["neck"],"emoji":"🧣","order":4153,"group":7,"version":5},{"shortcodes":["gloves"],"annotation":"gloves","tags":["hand"],"emoji":"🧤","order":4154,"group":7,"version":5},{"shortcodes":["coat"],"annotation":"coat","tags":["jacket"],"emoji":"🧥","order":4155,"group":7,"version":5},{"shortcodes":["socks"],"annotation":"socks","tags":["stocking"],"emoji":"🧦","order":4156,"group":7,"version":5},{"shortcodes":["dress"],"annotation":"dress","tags":["clothing"],"emoji":"👗","order":4157,"group":7,"version":0.6},{"shortcodes":["kimono"],"annotation":"kimono","tags":["clothing"],"emoji":"👘","order":4158,"group":7,"version":0.6},{"shortcodes":["sari"],"annotation":"sari","tags":["clothing","dress"],"emoji":"🥻","order":4159,"group":7,"version":12},{"shortcodes":["one_piece_swimsuit"],"annotation":"one-piece swimsuit","tags":["bathing suit"],"emoji":"🩱","order":4160,"group":7,"version":12},{"shortcodes":["briefs"],"annotation":"briefs","tags":["bathing suit","one-piece","swimsuit","underwear"],"emoji":"🩲","order":4161,"group":7,"version":12},{"shortcodes":["shorts"],"annotation":"shorts","tags":["bathing suit","pants","underwear"],"emoji":"🩳","order":4162,"group":7,"version":12},{"shortcodes":["bikini"],"annotation":"bikini","tags":["clothing","swim"],"emoji":"👙","order":4163,"group":7,"version":0.6},{"shortcodes":["womans_clothes"],"annotation":"woman’s clothes","tags":["clothing","woman"],"emoji":"👚","order":4164,"group":7,"version":0.6},{"shortcodes":["folding_fan"],"annotation":"folding hand fan","tags":["cooling","dance","fan","flutter","hot","shy"],"emoji":"🪭","order":4165,"group":7,"version":15},{"shortcodes":["purse"],"annotation":"purse","tags":["clothing","coin"],"emoji":"👛","order":4166,"group":7,"version":0.6},{"shortcodes":["handbag"],"annotation":"handbag","tags":["bag","clothing","purse"],"emoji":"👜","order":4167,"group":7,"version":0.6},{"shortcodes":["clutch_bag","pouch"],"annotation":"clutch bag","tags":["bag","clothing","pouch"],"emoji":"👝","order":4168,"group":7,"version":0.6},{"shortcodes":["shopping_bags"],"annotation":"shopping bags","tags":["bag","hotel","shopping"],"emoji":"🛍️","order":4170,"group":7,"version":0.7},{"shortcodes":["backpack","school_satchel"],"annotation":"backpack","tags":["bag","rucksack","satchel","school"],"emoji":"🎒","order":4171,"group":7,"version":0.6},{"shortcodes":["thong_sandal"],"annotation":"thong sandal","tags":["beach sandals","sandals","thong sandals","thongs","zōri"],"emoji":"🩴","order":4172,"group":7,"version":13},{"shortcodes":["mans_shoe"],"annotation":"man’s shoe","tags":["clothing","man","shoe"],"emoji":"👞","order":4173,"group":7,"version":0.6},{"shortcodes":["athletic_shoe","sneaker"],"annotation":"running shoe","tags":["athletic","clothing","shoe","sneaker"],"emoji":"👟","order":4174,"group":7,"version":0.6},{"shortcodes":["hiking_boot"],"annotation":"hiking boot","tags":["backpacking","boot","camping","hiking"],"emoji":"🥾","order":4175,"group":7,"version":11},{"shortcodes":["flat_shoe","womans_flat_shoe"],"annotation":"flat shoe","tags":["ballet flat","slip-on","slipper"],"emoji":"🥿","order":4176,"group":7,"version":11},{"shortcodes":["high_heel"],"annotation":"high-heeled shoe","tags":["clothing","heel","shoe","woman"],"emoji":"👠","order":4177,"group":7,"version":0.6},{"shortcodes":["sandal"],"annotation":"woman’s sandal","tags":["clothing","sandal","shoe","woman"],"emoji":"👡","order":4178,"group":7,"version":0.6},{"shortcodes":["ballet_shoes"],"annotation":"ballet shoes","tags":["ballet","dance"],"emoji":"🩰","order":4179,"group":7,"version":12},{"shortcodes":["boot"],"annotation":"woman’s boot","tags":["boot","clothing","shoe","woman"],"emoji":"👢","order":4180,"group":7,"version":0.6},{"shortcodes":["hair_pick"],"annotation":"hair pick","tags":["afro","comb","hair","pick"],"emoji":"🪮","order":4181,"group":7,"version":15},{"shortcodes":["crown"],"annotation":"crown","tags":["clothing","king","queen"],"emoji":"👑","order":4182,"group":7,"version":0.6},{"shortcodes":["womans_hat"],"annotation":"woman’s hat","tags":["clothing","hat","woman"],"emoji":"👒","order":4183,"group":7,"version":0.6},{"shortcodes":["top_hat","tophat"],"annotation":"top hat","tags":["clothing","hat","top","tophat"],"emoji":"🎩","order":4184,"group":7,"version":0.6},{"shortcodes":["graduation_cap","mortar_board"],"annotation":"graduation cap","tags":["cap","celebration","clothing","graduation","hat"],"emoji":"🎓️","order":4185,"group":7,"version":0.6},{"shortcodes":["billed_cap"],"annotation":"billed cap","tags":["baseball cap"],"emoji":"🧢","order":4186,"group":7,"version":5},{"shortcodes":["military_helmet"],"annotation":"military helmet","tags":["army","helmet","military","soldier","warrior"],"emoji":"🪖","order":4187,"group":7,"version":13},{"shortcodes":["helmet_with_cross","rescue_worker_helmet"],"annotation":"rescue worker’s helmet","tags":["aid","cross","face","hat","helmet"],"emoji":"⛑️","order":4189,"group":7,"version":0.7},{"shortcodes":["prayer_beads"],"annotation":"prayer beads","tags":["beads","clothing","necklace","prayer","religion"],"emoji":"📿","order":4190,"group":7,"version":1},{"shortcodes":["lipstick"],"annotation":"lipstick","tags":["cosmetics","makeup"],"emoji":"💄","order":4191,"group":7,"version":0.6},{"shortcodes":["ring"],"annotation":"ring","tags":["diamond"],"emoji":"💍","order":4192,"group":7,"version":0.6},{"shortcodes":["gem"],"annotation":"gem stone","tags":["diamond","gem","jewel"],"emoji":"💎","order":4193,"group":7,"version":0.6},{"shortcodes":["mute","no_sound"],"annotation":"muted speaker","tags":["mute","quiet","silent","speaker"],"emoji":"🔇","order":4194,"group":7,"version":1},{"shortcodes":["low_volume","quiet_sound","speaker"],"annotation":"speaker low volume","tags":["soft"],"emoji":"🔈️","order":4195,"group":7,"version":0.7},{"shortcodes":["medium_volumne","sound"],"annotation":"speaker medium volume","tags":["medium"],"emoji":"🔉","order":4196,"group":7,"version":1},{"shortcodes":["high_volume","loud_sound"],"annotation":"speaker high volume","tags":["loud"],"emoji":"🔊","order":4197,"group":7,"version":0.6},{"shortcodes":["loudspeaker"],"annotation":"loudspeaker","tags":["loud","public address"],"emoji":"📢","order":4198,"group":7,"version":0.6},{"shortcodes":["mega","megaphone"],"annotation":"megaphone","tags":["cheering"],"emoji":"📣","order":4199,"group":7,"version":0.6},{"shortcodes":["postal_horn"],"annotation":"postal horn","tags":["horn","post","postal"],"emoji":"📯","order":4200,"group":7,"version":1},{"shortcodes":["bell"],"annotation":"bell","tags":["bell"],"emoji":"🔔","order":4201,"group":7,"version":0.6},{"shortcodes":["no_bell"],"annotation":"bell with slash","tags":["bell","forbidden","mute","quiet","silent"],"emoji":"🔕","order":4202,"group":7,"version":1},{"shortcodes":["musical_score"],"annotation":"musical score","tags":["music","score"],"emoji":"🎼","order":4203,"group":7,"version":0.6},{"shortcodes":["musical_note"],"annotation":"musical note","tags":["music","note"],"emoji":"🎵","order":4204,"group":7,"version":0.6},{"shortcodes":["musical_notes","notes"],"annotation":"musical notes","tags":["music","note","notes"],"emoji":"🎶","order":4205,"group":7,"version":0.6},{"shortcodes":["studio_microphone"],"annotation":"studio microphone","tags":["mic","microphone","music","studio"],"emoji":"🎙️","order":4207,"group":7,"version":0.7},{"shortcodes":["level_slider"],"annotation":"level slider","tags":["level","music","slider"],"emoji":"🎚️","order":4209,"group":7,"version":0.7},{"shortcodes":["control_knobs"],"annotation":"control knobs","tags":["control","knobs","music"],"emoji":"🎛️","order":4211,"group":7,"version":0.7},{"shortcodes":["microphone"],"annotation":"microphone","tags":["karaoke","mic"],"emoji":"🎤","order":4212,"group":7,"version":0.6},{"shortcodes":["headphones"],"annotation":"headphone","tags":["earbud"],"emoji":"🎧️","order":4213,"group":7,"version":0.6},{"shortcodes":["radio"],"annotation":"radio","tags":["video"],"emoji":"📻️","order":4214,"group":7,"version":0.6},{"shortcodes":["saxophone"],"annotation":"saxophone","tags":["instrument","music","sax"],"emoji":"🎷","order":4215,"group":7,"version":0.6},{"shortcodes":["accordion"],"annotation":"accordion","tags":["concertina","squeeze box"],"emoji":"🪗","order":4216,"group":7,"version":13},{"shortcodes":["guitar"],"annotation":"guitar","tags":["instrument","music"],"emoji":"🎸","order":4217,"group":7,"version":0.6},{"shortcodes":["musical_keyboard"],"annotation":"musical keyboard","tags":["instrument","keyboard","music","piano"],"emoji":"🎹","order":4218,"group":7,"version":0.6},{"shortcodes":["trumpet"],"annotation":"trumpet","tags":["instrument","music"],"emoji":"🎺","order":4219,"group":7,"version":0.6},{"shortcodes":["violin"],"annotation":"violin","tags":["instrument","music"],"emoji":"🎻","order":4220,"group":7,"version":0.6},{"shortcodes":["banjo"],"annotation":"banjo","tags":["music","stringed"],"emoji":"🪕","order":4221,"group":7,"version":12},{"shortcodes":["drum"],"annotation":"drum","tags":["drumsticks","music"],"emoji":"🥁","order":4222,"group":7,"version":3},{"shortcodes":["long_drum"],"annotation":"long drum","tags":["beat","conga","drum","rhythm"],"emoji":"🪘","order":4223,"group":7,"version":13},{"shortcodes":["maracas"],"annotation":"maracas","tags":["instrument","music","percussion","rattle","shake"],"emoji":"🪇","order":4224,"group":7,"version":15},{"shortcodes":["flute"],"annotation":"flute","tags":["fife","music","pipe","recorder","woodwind"],"emoji":"🪈","order":4225,"group":7,"version":15},{"shortcodes":["android","iphone","mobile_phone"],"annotation":"mobile phone","tags":["cell","mobile","phone","telephone"],"emoji":"📱","order":4226,"group":7,"version":0.6},{"shortcodes":["calling","mobile_phone_arrow"],"annotation":"mobile phone with arrow","tags":["arrow","cell","mobile","phone","receive"],"emoji":"📲","order":4227,"group":7,"version":0.6},{"shortcodes":["telephone"],"annotation":"telephone","tags":["phone"],"emoji":"☎️","order":4229,"group":7,"version":0.6},{"shortcodes":["telephone_receiver"],"annotation":"telephone receiver","tags":["phone","receiver","telephone"],"emoji":"📞","order":4230,"group":7,"version":0.6},{"shortcodes":["pager"],"annotation":"pager","tags":["pager"],"emoji":"📟️","order":4231,"group":7,"version":0.6},{"shortcodes":["fax","fax_machine"],"annotation":"fax machine","tags":["fax"],"emoji":"📠","order":4232,"group":7,"version":0.6},{"shortcodes":["battery"],"annotation":"battery","tags":["battery"],"emoji":"🔋","order":4233,"group":7,"version":0.6},{"shortcodes":["low_battery"],"annotation":"low battery","tags":["electronic","low energy"],"emoji":"🪫","order":4234,"group":7,"version":14},{"shortcodes":["electric_plug"],"annotation":"electric plug","tags":["electric","electricity","plug"],"emoji":"🔌","order":4235,"group":7,"version":0.6},{"shortcodes":["laptop"],"annotation":"laptop","tags":["computer","pc","personal"],"emoji":"💻️","order":4236,"group":7,"version":0.6},{"shortcodes":["computer","desktop_computer"],"annotation":"desktop computer","tags":["computer","desktop"],"emoji":"🖥️","order":4238,"group":7,"version":0.7},{"shortcodes":["printer"],"annotation":"printer","tags":["computer"],"emoji":"🖨️","order":4240,"group":7,"version":0.7},{"shortcodes":["keyboard"],"annotation":"keyboard","tags":["computer"],"emoji":"⌨️","order":4242,"group":7,"version":1},{"shortcodes":["computer_mouse"],"annotation":"computer mouse","tags":["computer"],"emoji":"🖱️","order":4244,"group":7,"version":0.7},{"shortcodes":["trackball"],"annotation":"trackball","tags":["computer"],"emoji":"🖲️","order":4246,"group":7,"version":0.7},{"shortcodes":["computer_disk","minidisc"],"annotation":"computer disk","tags":["computer","disk","minidisk","optical"],"emoji":"💽","order":4247,"group":7,"version":0.6},{"shortcodes":["floppy_disk"],"annotation":"floppy disk","tags":["computer","disk","floppy"],"emoji":"💾","order":4248,"group":7,"version":0.6},{"shortcodes":["cd","optical_disk"],"annotation":"optical disk","tags":["cd","computer","disk","optical"],"emoji":"💿️","order":4249,"group":7,"version":0.6},{"shortcodes":["dvd"],"annotation":"dvd","tags":["blu-ray","computer","disk","optical"],"emoji":"📀","order":4250,"group":7,"version":0.6},{"shortcodes":["abacus"],"annotation":"abacus","tags":["calculation"],"emoji":"🧮","order":4251,"group":7,"version":11},{"shortcodes":["movie_camera"],"annotation":"movie camera","tags":["camera","cinema","movie"],"emoji":"🎥","order":4252,"group":7,"version":0.6},{"shortcodes":["film_frames"],"annotation":"film frames","tags":["cinema","film","frames","movie"],"emoji":"🎞️","order":4254,"group":7,"version":0.7},{"shortcodes":["film_projector"],"annotation":"film projector","tags":["cinema","film","movie","projector","video"],"emoji":"📽️","order":4256,"group":7,"version":0.7},{"shortcodes":["clapper"],"annotation":"clapper board","tags":["clapper","movie"],"emoji":"🎬️","order":4257,"group":7,"version":0.6},{"shortcodes":["tv"],"annotation":"television","tags":["tv","video"],"emoji":"📺️","order":4258,"group":7,"version":0.6},{"shortcodes":["camera"],"annotation":"camera","tags":["video"],"emoji":"📷️","order":4259,"group":7,"version":0.6},{"shortcodes":["camera_with_flash"],"annotation":"camera with flash","tags":["camera","flash","video"],"emoji":"📸","order":4260,"group":7,"version":1},{"shortcodes":["video_camera"],"annotation":"video camera","tags":["camera","video"],"emoji":"📹️","order":4261,"group":7,"version":0.6},{"shortcodes":["vhs","videocassette"],"annotation":"videocassette","tags":["tape","vhs","video"],"emoji":"📼","order":4262,"group":7,"version":0.6},{"shortcodes":["mag"],"annotation":"magnifying glass tilted left","tags":["glass","magnifying","search","tool"],"emoji":"🔍️","order":4263,"group":7,"version":0.6},{"shortcodes":["mag_right"],"annotation":"magnifying glass tilted right","tags":["glass","magnifying","search","tool"],"emoji":"🔎","order":4264,"group":7,"version":0.6},{"shortcodes":["candle"],"annotation":"candle","tags":["light"],"emoji":"🕯️","order":4266,"group":7,"version":0.7},{"shortcodes":["bulb","light_bulb"],"annotation":"light bulb","tags":["bulb","comic","electric","idea","light"],"emoji":"💡","order":4267,"group":7,"version":0.6},{"shortcodes":["flashlight"],"annotation":"flashlight","tags":["electric","light","tool","torch"],"emoji":"🔦","order":4268,"group":7,"version":0.6},{"shortcodes":["izakaya_lantern","red_paper_lantern"],"annotation":"red paper lantern","tags":["bar","lantern","light","red"],"emoji":"🏮","order":4269,"group":7,"version":0.6},{"shortcodes":["diya_lamp"],"annotation":"diya lamp","tags":["diya","lamp","oil"],"emoji":"🪔","order":4270,"group":7,"version":12},{"shortcodes":["notebook_with_decorative_cover"],"annotation":"notebook with decorative cover","tags":["book","cover","decorated","notebook"],"emoji":"📔","order":4271,"group":7,"version":0.6},{"shortcodes":["closed_book"],"annotation":"closed book","tags":["book","closed"],"emoji":"📕","order":4272,"group":7,"version":0.6},{"shortcodes":["book","open_book"],"annotation":"open book","tags":["book","open"],"emoji":"📖","order":4273,"group":7,"version":0.6},{"shortcodes":["green_book"],"annotation":"green book","tags":["book","green"],"emoji":"📗","order":4274,"group":7,"version":0.6},{"shortcodes":["blue_book"],"annotation":"blue book","tags":["blue","book"],"emoji":"📘","order":4275,"group":7,"version":0.6},{"shortcodes":["orange_book"],"annotation":"orange book","tags":["book","orange"],"emoji":"📙","order":4276,"group":7,"version":0.6},{"shortcodes":["books"],"annotation":"books","tags":["book"],"emoji":"📚️","order":4277,"group":7,"version":0.6},{"shortcodes":["notebook"],"annotation":"notebook","tags":["notebook"],"emoji":"📓","order":4278,"group":7,"version":0.6},{"shortcodes":["ledger"],"annotation":"ledger","tags":["notebook"],"emoji":"📒","order":4279,"group":7,"version":0.6},{"shortcodes":["page_with_curl"],"annotation":"page with curl","tags":["curl","document","page"],"emoji":"📃","order":4280,"group":7,"version":0.6},{"shortcodes":["scroll"],"annotation":"scroll","tags":["paper"],"emoji":"📜","order":4281,"group":7,"version":0.6},{"shortcodes":["page_facing_up"],"annotation":"page facing up","tags":["document","page"],"emoji":"📄","order":4282,"group":7,"version":0.6},{"shortcodes":["newspaper"],"annotation":"newspaper","tags":["news","paper"],"emoji":"📰","order":4283,"group":7,"version":0.6},{"shortcodes":["rolled_up_newspaper"],"annotation":"rolled-up newspaper","tags":["news","newspaper","paper","rolled"],"emoji":"🗞️","order":4285,"group":7,"version":0.7},{"shortcodes":["bookmark_tabs"],"annotation":"bookmark tabs","tags":["bookmark","mark","marker","tabs"],"emoji":"📑","order":4286,"group":7,"version":0.6},{"shortcodes":["bookmark"],"annotation":"bookmark","tags":["mark"],"emoji":"🔖","order":4287,"group":7,"version":0.6},{"shortcodes":["label"],"annotation":"label","tags":["label"],"emoji":"🏷️","order":4289,"group":7,"version":0.7},{"shortcodes":["moneybag"],"annotation":"money bag","tags":["bag","dollar","money","moneybag"],"emoji":"💰️","order":4290,"group":7,"version":0.6},{"shortcodes":["coin"],"annotation":"coin","tags":["gold","metal","money","silver","treasure"],"emoji":"🪙","order":4291,"group":7,"version":13},{"shortcodes":["yen"],"annotation":"yen banknote","tags":["banknote","bill","currency","money","note","yen"],"emoji":"💴","order":4292,"group":7,"version":0.6},{"shortcodes":["dollar"],"annotation":"dollar banknote","tags":["banknote","bill","currency","dollar","money","note"],"emoji":"💵","order":4293,"group":7,"version":0.6},{"shortcodes":["euro"],"annotation":"euro banknote","tags":["banknote","bill","currency","euro","money","note"],"emoji":"💶","order":4294,"group":7,"version":1},{"shortcodes":["pound"],"annotation":"pound banknote","tags":["banknote","bill","currency","money","note","pound"],"emoji":"💷","order":4295,"group":7,"version":1},{"shortcodes":["money_with_wings"],"annotation":"money with wings","tags":["banknote","bill","fly","money","wings"],"emoji":"💸","order":4296,"group":7,"version":0.6},{"shortcodes":["credit_card"],"annotation":"credit card","tags":["card","credit","money"],"emoji":"💳️","order":4297,"group":7,"version":0.6},{"shortcodes":["receipt"],"annotation":"receipt","tags":["accounting","bookkeeping","evidence","proof"],"emoji":"🧾","order":4298,"group":7,"version":11},{"shortcodes":["chart"],"annotation":"chart increasing with yen","tags":["chart","graph","growth","money","yen"],"emoji":"💹","order":4299,"group":7,"version":0.6},{"shortcodes":["envelope"],"annotation":"envelope","tags":["email","letter"],"emoji":"✉️","order":4301,"group":7,"version":0.6},{"shortcodes":["e-mail","email"],"annotation":"e-mail","tags":["email","letter","mail"],"emoji":"📧","order":4302,"group":7,"version":0.6},{"shortcodes":["incoming_envelope"],"annotation":"incoming envelope","tags":["e-mail","email","envelope","incoming","letter","receive"],"emoji":"📨","order":4303,"group":7,"version":0.6},{"shortcodes":["envelope_with_arrow"],"annotation":"envelope with arrow","tags":["arrow","e-mail","email","envelope","outgoing"],"emoji":"📩","order":4304,"group":7,"version":0.6},{"shortcodes":["outbox_tray"],"annotation":"outbox tray","tags":["box","letter","mail","outbox","sent","tray"],"emoji":"📤️","order":4305,"group":7,"version":0.6},{"shortcodes":["inbox_tray"],"annotation":"inbox tray","tags":["box","inbox","letter","mail","receive","tray"],"emoji":"📥️","order":4306,"group":7,"version":0.6},{"shortcodes":["package"],"annotation":"package","tags":["box","parcel"],"emoji":"📦️","order":4307,"group":7,"version":0.6},{"shortcodes":["mailbox"],"annotation":"closed mailbox with raised flag","tags":["closed","mail","mailbox","postbox"],"emoji":"📫️","order":4308,"group":7,"version":0.6},{"shortcodes":["mailbox_closed"],"annotation":"closed mailbox with lowered flag","tags":["closed","lowered","mail","mailbox","postbox"],"emoji":"📪️","order":4309,"group":7,"version":0.6},{"shortcodes":["mailbox_with_mail"],"annotation":"open mailbox with raised flag","tags":["mail","mailbox","open","postbox"],"emoji":"📬️","order":4310,"group":7,"version":0.7},{"shortcodes":["mailbox_with_no_mail"],"annotation":"open mailbox with lowered flag","tags":["lowered","mail","mailbox","open","postbox"],"emoji":"📭️","order":4311,"group":7,"version":0.7},{"shortcodes":["postbox"],"annotation":"postbox","tags":["mail","mailbox"],"emoji":"📮","order":4312,"group":7,"version":0.6},{"shortcodes":["ballot_box"],"annotation":"ballot box with ballot","tags":["ballot","box"],"emoji":"🗳️","order":4314,"group":7,"version":0.7},{"shortcodes":["pencil"],"annotation":"pencil","tags":["pencil"],"emoji":"✏️","order":4316,"group":7,"version":0.6},{"shortcodes":["black_nib"],"annotation":"black nib","tags":["nib","pen"],"emoji":"✒️","order":4318,"group":7,"version":0.6},{"shortcodes":["fountain_pen"],"annotation":"fountain pen","tags":["fountain","pen"],"emoji":"🖋️","order":4320,"group":7,"version":0.7},{"shortcodes":["pen"],"annotation":"pen","tags":["ballpoint"],"emoji":"🖊️","order":4322,"group":7,"version":0.7},{"shortcodes":["paintbrush"],"annotation":"paintbrush","tags":["painting"],"emoji":"🖌️","order":4324,"group":7,"version":0.7},{"shortcodes":["crayon"],"annotation":"crayon","tags":["crayon"],"emoji":"🖍️","order":4326,"group":7,"version":0.7},{"shortcodes":["memo"],"annotation":"memo","tags":["pencil"],"emoji":"📝","order":4327,"group":7,"version":0.6},{"shortcodes":["briefcase"],"annotation":"briefcase","tags":["briefcase"],"emoji":"💼","order":4328,"group":7,"version":0.6},{"shortcodes":["file_folder"],"annotation":"file folder","tags":["file","folder"],"emoji":"📁","order":4329,"group":7,"version":0.6},{"shortcodes":["open_file_folder"],"annotation":"open file folder","tags":["file","folder","open"],"emoji":"📂","order":4330,"group":7,"version":0.6},{"shortcodes":["card_index_dividers"],"annotation":"card index dividers","tags":["card","dividers","index"],"emoji":"🗂️","order":4332,"group":7,"version":0.7},{"shortcodes":["date"],"annotation":"calendar","tags":["date"],"emoji":"📅","order":4333,"group":7,"version":0.6},{"shortcodes":["calendar"],"annotation":"tear-off calendar","tags":["calendar"],"emoji":"📆","order":4334,"group":7,"version":0.6},{"shortcodes":["notepad_spiral"],"annotation":"spiral notepad","tags":["note","pad","spiral"],"emoji":"🗒️","order":4336,"group":7,"version":0.7},{"shortcodes":["calendar_spiral"],"annotation":"spiral calendar","tags":["calendar","pad","spiral"],"emoji":"🗓️","order":4338,"group":7,"version":0.7},{"shortcodes":["card_index"],"annotation":"card index","tags":["card","index","rolodex"],"emoji":"📇","order":4339,"group":7,"version":0.6},{"shortcodes":["chart_increasing","chart_with_upwards_trend"],"annotation":"chart increasing","tags":["chart","graph","growth","trend","upward"],"emoji":"📈","order":4340,"group":7,"version":0.6},{"shortcodes":["chart_decreasing","chart_with_downwards_trend"],"annotation":"chart decreasing","tags":["chart","down","graph","trend"],"emoji":"📉","order":4341,"group":7,"version":0.6},{"shortcodes":["bar_chart"],"annotation":"bar chart","tags":["bar","chart","graph"],"emoji":"📊","order":4342,"group":7,"version":0.6},{"shortcodes":["clipboard"],"annotation":"clipboard","tags":["clipboard"],"emoji":"📋️","order":4343,"group":7,"version":0.6},{"shortcodes":["pushpin"],"annotation":"pushpin","tags":["pin"],"emoji":"📌","order":4344,"group":7,"version":0.6},{"shortcodes":["round_pushpin"],"annotation":"round pushpin","tags":["pin","pushpin"],"emoji":"📍","order":4345,"group":7,"version":0.6},{"shortcodes":["paperclip"],"annotation":"paperclip","tags":["paperclip"],"emoji":"📎","order":4346,"group":7,"version":0.6},{"shortcodes":["paperclips"],"annotation":"linked paperclips","tags":["link","paperclip"],"emoji":"🖇️","order":4348,"group":7,"version":0.7},{"shortcodes":["straight_ruler"],"annotation":"straight ruler","tags":["ruler","straight edge"],"emoji":"📏","order":4349,"group":7,"version":0.6},{"shortcodes":["triangular_ruler"],"annotation":"triangular ruler","tags":["ruler","set","triangle"],"emoji":"📐","order":4350,"group":7,"version":0.6},{"shortcodes":["scissors"],"annotation":"scissors","tags":["cutting","tool"],"emoji":"✂️","order":4352,"group":7,"version":0.6},{"shortcodes":["card_file_box"],"annotation":"card file box","tags":["box","card","file"],"emoji":"🗃️","order":4354,"group":7,"version":0.7},{"shortcodes":["file_cabinet"],"annotation":"file cabinet","tags":["cabinet","file","filing"],"emoji":"🗄️","order":4356,"group":7,"version":0.7},{"shortcodes":["trashcan","wastebasket"],"annotation":"wastebasket","tags":["wastebasket"],"emoji":"🗑️","order":4358,"group":7,"version":0.7},{"shortcodes":["lock","locked"],"annotation":"locked","tags":["closed"],"emoji":"🔒️","order":4359,"group":7,"version":0.6},{"shortcodes":["unlock","unlocked"],"annotation":"unlocked","tags":["lock","open","unlock"],"emoji":"🔓️","order":4360,"group":7,"version":0.6},{"shortcodes":["lock_with_ink_pen","locked_with_pen"],"annotation":"locked with pen","tags":["ink","lock","nib","pen","privacy"],"emoji":"🔏","order":4361,"group":7,"version":0.6},{"shortcodes":["closed_lock_with_key","locked_with_key"],"annotation":"locked with key","tags":["closed","key","lock","secure"],"emoji":"🔐","order":4362,"group":7,"version":0.6},{"shortcodes":["key"],"annotation":"key","tags":["lock","password"],"emoji":"🔑","order":4363,"group":7,"version":0.6},{"shortcodes":["old_key"],"annotation":"old key","tags":["clue","key","lock","old"],"emoji":"🗝️","order":4365,"group":7,"version":0.7},{"shortcodes":["hammer"],"annotation":"hammer","tags":["tool"],"emoji":"🔨","order":4366,"group":7,"version":0.6},{"shortcodes":["axe"],"annotation":"axe","tags":["chop","hatchet","split","wood"],"emoji":"🪓","order":4367,"group":7,"version":12},{"shortcodes":["pick"],"annotation":"pick","tags":["mining","tool"],"emoji":"⛏️","order":4369,"group":7,"version":0.7},{"shortcodes":["hammer_and_pick"],"annotation":"hammer and pick","tags":["hammer","pick","tool"],"emoji":"⚒️","order":4371,"group":7,"version":1},{"shortcodes":["hammer_and_wrench"],"annotation":"hammer and wrench","tags":["hammer","spanner","tool","wrench"],"emoji":"🛠️","order":4373,"group":7,"version":0.7},{"shortcodes":["dagger"],"annotation":"dagger","tags":["knife","weapon"],"emoji":"🗡️","order":4375,"group":7,"version":0.7},{"shortcodes":["crossed_swords"],"annotation":"crossed swords","tags":["crossed","swords","weapon"],"emoji":"⚔️","order":4377,"group":7,"version":1},{"shortcodes":["bomb"],"annotation":"bomb","tags":["comic"],"emoji":"💣️","order":4378,"group":7,"version":0.6},{"shortcodes":["boomerang"],"annotation":"boomerang","tags":["rebound","repercussion"],"emoji":"🪃","order":4379,"group":7,"version":13},{"shortcodes":["bow_and_arrow"],"annotation":"bow and arrow","tags":["archer","arrow","bow","sagittarius","zodiac"],"emoji":"🏹","order":4380,"group":7,"version":1},{"shortcodes":["shield"],"annotation":"shield","tags":["weapon"],"emoji":"🛡️","order":4382,"group":7,"version":0.7},{"shortcodes":["carpentry_saw"],"annotation":"carpentry saw","tags":["carpenter","lumber","saw","tool"],"emoji":"🪚","order":4383,"group":7,"version":13},{"shortcodes":["wrench"],"annotation":"wrench","tags":["spanner","tool"],"emoji":"🔧","order":4384,"group":7,"version":0.6},{"shortcodes":["screwdriver"],"annotation":"screwdriver","tags":["screw","tool"],"emoji":"🪛","order":4385,"group":7,"version":13},{"shortcodes":["nut_and_bolt"],"annotation":"nut and bolt","tags":["bolt","nut","tool"],"emoji":"🔩","order":4386,"group":7,"version":0.6},{"shortcodes":["gear"],"annotation":"gear","tags":["cog","cogwheel","tool"],"emoji":"⚙️","order":4388,"group":7,"version":1},{"shortcodes":["clamp","compression"],"annotation":"clamp","tags":["compress","tool","vice"],"emoji":"🗜️","order":4390,"group":7,"version":0.7},{"shortcodes":["scales"],"annotation":"balance scale","tags":["balance","justice","libra","scale","zodiac"],"emoji":"⚖️","order":4392,"group":7,"version":1},{"shortcodes":["probing_cane","white_cane"],"annotation":"white cane","tags":["accessibility","blind"],"emoji":"🦯","order":4393,"group":7,"version":12},{"shortcodes":["link"],"annotation":"link","tags":["link"],"emoji":"🔗","order":4394,"group":7,"version":0.6},{"shortcodes":["broken_chain"],"annotation":"broken chain","tags":["break","breaking","chain","cuffs","freedom"],"emoji":"⛓️‍💥","order":4395,"group":7,"version":15.1},{"shortcodes":["chains"],"annotation":"chains","tags":["chain"],"emoji":"⛓️","order":4398,"group":7,"version":0.7},{"shortcodes":["hook"],"annotation":"hook","tags":["catch","crook","curve","ensnare","selling point"],"emoji":"🪝","order":4399,"group":7,"version":13},{"shortcodes":["toolbox"],"annotation":"toolbox","tags":["chest","mechanic","tool"],"emoji":"🧰","order":4400,"group":7,"version":11},{"shortcodes":["magnet"],"annotation":"magnet","tags":["attraction","horseshoe","magnetic"],"emoji":"🧲","order":4401,"group":7,"version":11},{"shortcodes":["ladder"],"annotation":"ladder","tags":["climb","rung","step"],"emoji":"🪜","order":4402,"group":7,"version":13},{"shortcodes":["alembic"],"annotation":"alembic","tags":["chemistry","tool"],"emoji":"⚗️","order":4404,"group":7,"version":1},{"shortcodes":["test_tube"],"annotation":"test tube","tags":["chemist","chemistry","experiment","lab","science"],"emoji":"🧪","order":4405,"group":7,"version":11},{"shortcodes":["petri_dish"],"annotation":"petri dish","tags":["bacteria","biologist","biology","culture","lab"],"emoji":"🧫","order":4406,"group":7,"version":11},{"shortcodes":["dna","double_helix"],"annotation":"dna","tags":["biologist","evolution","gene","genetics","life"],"emoji":"🧬","order":4407,"group":7,"version":11},{"shortcodes":["microscope"],"annotation":"microscope","tags":["science","tool"],"emoji":"🔬","order":4408,"group":7,"version":1},{"shortcodes":["telescope"],"annotation":"telescope","tags":["science","tool"],"emoji":"🔭","order":4409,"group":7,"version":1},{"shortcodes":["satellite_antenna"],"annotation":"satellite antenna","tags":["antenna","dish","satellite"],"emoji":"📡","order":4410,"group":7,"version":0.6},{"shortcodes":["syringe"],"annotation":"syringe","tags":["medicine","needle","shot","sick"],"emoji":"💉","order":4411,"group":7,"version":0.6},{"shortcodes":["drop_of_blood"],"annotation":"drop of blood","tags":["bleed","blood donation","injury","medicine","menstruation"],"emoji":"🩸","order":4412,"group":7,"version":12},{"shortcodes":["pill"],"annotation":"pill","tags":["doctor","medicine","sick"],"emoji":"💊","order":4413,"group":7,"version":0.6},{"shortcodes":["adhesive_bandage","bandaid"],"annotation":"adhesive bandage","tags":["bandage"],"emoji":"🩹","order":4414,"group":7,"version":12},{"shortcodes":["crutch"],"annotation":"crutch","tags":["cane","disability","hurt","mobility aid","stick"],"emoji":"🩼","order":4415,"group":7,"version":14},{"shortcodes":["stethoscope"],"annotation":"stethoscope","tags":["doctor","heart","medicine"],"emoji":"🩺","order":4416,"group":7,"version":12},{"shortcodes":["x-ray","xray"],"annotation":"x-ray","tags":["bones","doctor","medical","skeleton"],"emoji":"🩻","order":4417,"group":7,"version":14},{"shortcodes":["door"],"annotation":"door","tags":["door"],"emoji":"🚪","order":4418,"group":7,"version":0.6},{"shortcodes":["elevator"],"annotation":"elevator","tags":["accessibility","hoist","lift"],"emoji":"🛗","order":4419,"group":7,"version":13},{"shortcodes":["mirror"],"annotation":"mirror","tags":["reflection","reflector","speculum"],"emoji":"🪞","order":4420,"group":7,"version":13},{"shortcodes":["window"],"annotation":"window","tags":["frame","fresh air","opening","transparent","view"],"emoji":"🪟","order":4421,"group":7,"version":13},{"shortcodes":["bed"],"annotation":"bed","tags":["hotel","sleep"],"emoji":"🛏️","order":4423,"group":7,"version":0.7},{"shortcodes":["couch_and_lamp"],"annotation":"couch and lamp","tags":["couch","hotel","lamp"],"emoji":"🛋️","order":4425,"group":7,"version":0.7},{"shortcodes":["chair"],"annotation":"chair","tags":["seat","sit"],"emoji":"🪑","order":4426,"group":7,"version":12},{"shortcodes":["toilet"],"annotation":"toilet","tags":["toilet"],"emoji":"🚽","order":4427,"group":7,"version":0.6},{"shortcodes":["plunger"],"annotation":"plunger","tags":["force cup","plumber","suction","toilet"],"emoji":"🪠","order":4428,"group":7,"version":13},{"shortcodes":["shower"],"annotation":"shower","tags":["water"],"emoji":"🚿","order":4429,"group":7,"version":1},{"shortcodes":["bathtub"],"annotation":"bathtub","tags":["bath"],"emoji":"🛁","order":4430,"group":7,"version":1},{"shortcodes":["mouse_trap"],"annotation":"mouse trap","tags":["bait","mousetrap","snare","trap"],"emoji":"🪤","order":4431,"group":7,"version":13},{"shortcodes":["razor"],"annotation":"razor","tags":["sharp","shave"],"emoji":"🪒","order":4432,"group":7,"version":12},{"shortcodes":["lotion_bottle"],"annotation":"lotion bottle","tags":["lotion","moisturizer","shampoo","sunscreen"],"emoji":"🧴","order":4433,"group":7,"version":11},{"shortcodes":["safety_pin"],"annotation":"safety pin","tags":["diaper","punk rock"],"emoji":"🧷","order":4434,"group":7,"version":11},{"shortcodes":["broom"],"annotation":"broom","tags":["cleaning","sweeping","witch"],"emoji":"🧹","order":4435,"group":7,"version":11},{"shortcodes":["basket"],"annotation":"basket","tags":["farming","laundry","picnic"],"emoji":"🧺","order":4436,"group":7,"version":11},{"shortcodes":["roll_of_paper","toilet_paper"],"annotation":"roll of paper","tags":["paper towels","toilet paper"],"emoji":"🧻","order":4437,"group":7,"version":11},{"shortcodes":["bucket"],"annotation":"bucket","tags":["cask","pail","vat"],"emoji":"🪣","order":4438,"group":7,"version":13},{"shortcodes":["soap"],"annotation":"soap","tags":["bar","bathing","cleaning","lather","soapdish"],"emoji":"🧼","order":4439,"group":7,"version":11},{"shortcodes":["bubbles"],"annotation":"bubbles","tags":["burp","clean","soap","underwater"],"emoji":"🫧","order":4440,"group":7,"version":14},{"shortcodes":["toothbrush"],"annotation":"toothbrush","tags":["bathroom","brush","clean","dental","hygiene","teeth"],"emoji":"🪥","order":4441,"group":7,"version":13},{"shortcodes":["sponge"],"annotation":"sponge","tags":["absorbing","cleaning","porous"],"emoji":"🧽","order":4442,"group":7,"version":11},{"shortcodes":["fire_extinguisher"],"annotation":"fire extinguisher","tags":["extinguish","fire","quench"],"emoji":"🧯","order":4443,"group":7,"version":11},{"shortcodes":["shopping_cart"],"annotation":"shopping cart","tags":["cart","shopping","trolley"],"emoji":"🛒","order":4444,"group":7,"version":3},{"shortcodes":["cigarette","smoking"],"annotation":"cigarette","tags":["smoking"],"emoji":"🚬","order":4445,"group":7,"version":0.6},{"shortcodes":["coffin"],"annotation":"coffin","tags":["death"],"emoji":"⚰️","order":4447,"group":7,"version":1},{"shortcodes":["headstone"],"annotation":"headstone","tags":["cemetery","grave","graveyard","tombstone"],"emoji":"🪦","order":4448,"group":7,"version":13},{"shortcodes":["funeral_urn"],"annotation":"funeral urn","tags":["ashes","death","funeral","urn"],"emoji":"⚱️","order":4450,"group":7,"version":1},{"shortcodes":["nazar_amulet"],"annotation":"nazar amulet","tags":["bead","charm","evil-eye","nazar","talisman"],"emoji":"🧿","order":4451,"group":7,"version":11},{"shortcodes":["hamsa"],"annotation":"hamsa","tags":["amulet","fatima","hand","mary","miriam","protection"],"emoji":"🪬","order":4452,"group":7,"version":14},{"shortcodes":["moai","moyai"],"annotation":"moai","tags":["face","moyai","statue"],"emoji":"🗿","order":4453,"group":7,"version":0.6},{"shortcodes":["placard"],"annotation":"placard","tags":["demonstration","picket","protest","sign"],"emoji":"🪧","order":4454,"group":7,"version":13},{"shortcodes":["id_card"],"annotation":"identification card","tags":["credentials","id","license","security"],"emoji":"🪪","order":4455,"group":7,"version":14},{"shortcodes":["atm"],"annotation":"ATM sign","tags":["atm","atm sign","automated","bank","teller"],"emoji":"🏧","order":4456,"group":8,"version":0.6},{"shortcodes":["litter_bin","put_litter_in_its_place"],"annotation":"litter in bin sign","tags":["litter","litter bin"],"emoji":"🚮","order":4457,"group":8,"version":1},{"shortcodes":["potable_water"],"annotation":"potable water","tags":["drinking","potable","water"],"emoji":"🚰","order":4458,"group":8,"version":1},{"shortcodes":["handicapped","wheelchair"],"annotation":"wheelchair symbol","tags":["access"],"emoji":"♿️","order":4459,"group":8,"version":0.6},{"shortcodes":["mens"],"annotation":"men’s room","tags":["bathroom","lavatory","man","restroom","toilet","wc"],"emoji":"🚹️","order":4460,"group":8,"version":0.6},{"shortcodes":["womens"],"annotation":"women’s room","tags":["bathroom","lavatory","restroom","toilet","wc","woman"],"emoji":"🚺️","order":4461,"group":8,"version":0.6},{"shortcodes":["bathroom","restroom"],"annotation":"restroom","tags":["bathroom","lavatory","toilet","wc"],"emoji":"🚻","order":4462,"group":8,"version":0.6},{"shortcodes":["baby_symbol"],"annotation":"baby symbol","tags":["baby","changing"],"emoji":"🚼️","order":4463,"group":8,"version":0.6},{"shortcodes":["water_closet","wc"],"annotation":"water closet","tags":["bathroom","closet","lavatory","restroom","toilet","water","wc"],"emoji":"🚾","order":4464,"group":8,"version":0.6},{"shortcodes":["passport_control"],"annotation":"passport control","tags":["control","passport"],"emoji":"🛂","order":4465,"group":8,"version":1},{"shortcodes":["customs"],"annotation":"customs","tags":["customs"],"emoji":"🛃","order":4466,"group":8,"version":1},{"shortcodes":["baggage_claim"],"annotation":"baggage claim","tags":["baggage","claim"],"emoji":"🛄","order":4467,"group":8,"version":1},{"shortcodes":["left_luggage"],"annotation":"left luggage","tags":["baggage","locker","luggage"],"emoji":"🛅","order":4468,"group":8,"version":1},{"shortcodes":["warning"],"annotation":"warning","tags":["warning"],"emoji":"⚠️","order":4470,"group":8,"version":0.6},{"shortcodes":["children_crossing"],"annotation":"children crossing","tags":["child","crossing","pedestrian","traffic"],"emoji":"🚸","order":4471,"group":8,"version":1},{"shortcodes":["no_entry"],"annotation":"no entry","tags":["entry","forbidden","no","not","prohibited","traffic"],"emoji":"⛔️","order":4472,"group":8,"version":0.6},{"shortcodes":["no_entry_sign"],"annotation":"prohibited","tags":["entry","forbidden","no","not"],"emoji":"🚫","order":4473,"group":8,"version":0.6},{"shortcodes":["no_bicycles"],"annotation":"no bicycles","tags":["bicycle","bike","forbidden","no","prohibited"],"emoji":"🚳","order":4474,"group":8,"version":1},{"shortcodes":["no_smoking"],"annotation":"no smoking","tags":["forbidden","no","not","prohibited","smoking"],"emoji":"🚭️","order":4475,"group":8,"version":0.6},{"shortcodes":["do_not_litter","no_littering"],"annotation":"no littering","tags":["forbidden","litter","no","not","prohibited"],"emoji":"🚯","order":4476,"group":8,"version":1},{"shortcodes":["non-potable_water"],"annotation":"non-potable water","tags":["non-drinking","non-potable","water"],"emoji":"🚱","order":4477,"group":8,"version":1},{"shortcodes":["no_pedestrians"],"annotation":"no pedestrians","tags":["forbidden","no","not","pedestrian","prohibited"],"emoji":"🚷","order":4478,"group":8,"version":1},{"shortcodes":["no_mobile_phones"],"annotation":"no mobile phones","tags":["cell","forbidden","mobile","no","phone"],"emoji":"📵","order":4479,"group":8,"version":1},{"shortcodes":["no_one_under_18","underage"],"annotation":"no one under eighteen","tags":["18","age restriction","eighteen","prohibited","underage"],"emoji":"🔞","order":4480,"group":8,"version":0.6},{"shortcodes":["radioactive"],"annotation":"radioactive","tags":["sign"],"emoji":"☢️","order":4482,"group":8,"version":1},{"shortcodes":["biohazard"],"annotation":"biohazard","tags":["sign"],"emoji":"☣️","order":4484,"group":8,"version":1},{"shortcodes":["arrow_up"],"annotation":"up arrow","tags":["arrow","cardinal","direction","north"],"emoji":"⬆️","order":4486,"group":8,"version":0.6},{"shortcodes":["arrow_upper_right"],"annotation":"up-right arrow","tags":["arrow","direction","intercardinal","northeast"],"emoji":"↗️","order":4488,"group":8,"version":0.6},{"shortcodes":["arrow_right"],"annotation":"right arrow","tags":["arrow","cardinal","direction","east"],"emoji":"➡️","order":4490,"group":8,"version":0.6},{"shortcodes":["arrow_lower_right"],"annotation":"down-right arrow","tags":["arrow","direction","intercardinal","southeast"],"emoji":"↘️","order":4492,"group":8,"version":0.6},{"shortcodes":["arrow_down"],"annotation":"down arrow","tags":["arrow","cardinal","direction","down","south"],"emoji":"⬇️","order":4494,"group":8,"version":0.6},{"shortcodes":["arrow_lower_left"],"annotation":"down-left arrow","tags":["arrow","direction","intercardinal","southwest"],"emoji":"↙️","order":4496,"group":8,"version":0.6},{"shortcodes":["arrow_left"],"annotation":"left arrow","tags":["arrow","cardinal","direction","west"],"emoji":"⬅️","order":4498,"group":8,"version":0.6},{"shortcodes":["arrow_upper_left"],"annotation":"up-left arrow","tags":["arrow","direction","intercardinal","northwest"],"emoji":"↖️","order":4500,"group":8,"version":0.6},{"shortcodes":["arrow_up_down"],"annotation":"up-down arrow","tags":["arrow"],"emoji":"↕️","order":4502,"group":8,"version":0.6},{"shortcodes":["left_right_arrow"],"annotation":"left-right arrow","tags":["arrow"],"emoji":"↔️","order":4504,"group":8,"version":0.6},{"shortcodes":["arrow_left_hook","leftwards_arrow_with_hook"],"annotation":"right arrow curving left","tags":["arrow"],"emoji":"↩️","order":4506,"group":8,"version":0.6},{"shortcodes":["arrow_right_hook","rightwards_arrow_with_hook"],"annotation":"left arrow curving right","tags":["arrow"],"emoji":"↪️","order":4508,"group":8,"version":0.6},{"shortcodes":["arrow_heading_up"],"annotation":"right arrow curving up","tags":["arrow"],"emoji":"⤴️","order":4510,"group":8,"version":0.6},{"shortcodes":["arrow_heading_down"],"annotation":"right arrow curving down","tags":["arrow","down"],"emoji":"⤵️","order":4512,"group":8,"version":0.6},{"shortcodes":["arrows_clockwise","clockwise"],"annotation":"clockwise vertical arrows","tags":["arrow","clockwise","reload"],"emoji":"🔃","order":4513,"group":8,"version":0.6},{"shortcodes":["arrows_counterclockwise","counterclockwise"],"annotation":"counterclockwise arrows button","tags":["anticlockwise","arrow","counterclockwise","withershins"],"emoji":"🔄","order":4514,"group":8,"version":1},{"shortcodes":["back"],"annotation":"BACK arrow","tags":["arrow","back"],"emoji":"🔙","order":4515,"group":8,"version":0.6},{"shortcodes":["end"],"annotation":"END arrow","tags":["arrow","end"],"emoji":"🔚","order":4516,"group":8,"version":0.6},{"shortcodes":["on"],"annotation":"ON! arrow","tags":["arrow","mark","on","on!"],"emoji":"🔛","order":4517,"group":8,"version":0.6},{"shortcodes":["soon"],"annotation":"SOON arrow","tags":["arrow","soon"],"emoji":"🔜","order":4518,"group":8,"version":0.6},{"shortcodes":["top"],"annotation":"TOP arrow","tags":["arrow","top","up"],"emoji":"🔝","order":4519,"group":8,"version":0.6},{"shortcodes":["place_of_worship"],"annotation":"place of worship","tags":["religion","worship"],"emoji":"🛐","order":4520,"group":8,"version":1},{"shortcodes":["atom","atom_symbol"],"annotation":"atom symbol","tags":["atheist","atom"],"emoji":"⚛️","order":4522,"group":8,"version":1},{"shortcodes":["om"],"annotation":"om","tags":["hindu","religion"],"emoji":"🕉️","order":4524,"group":8,"version":0.7},{"shortcodes":["star_of_david"],"annotation":"star of David","tags":["david","jew","jewish","religion","star","star of david"],"emoji":"✡️","order":4526,"group":8,"version":0.7},{"shortcodes":["wheel_of_dharma"],"annotation":"wheel of dharma","tags":["buddhist","dharma","religion","wheel"],"emoji":"☸️","order":4528,"group":8,"version":0.7},{"shortcodes":["yin_yang"],"annotation":"yin yang","tags":["religion","tao","taoist","yang","yin"],"emoji":"☯️","order":4530,"group":8,"version":0.7},{"shortcodes":["latin_cross"],"annotation":"latin cross","tags":["christian","cross","religion"],"emoji":"✝️","order":4532,"group":8,"version":0.7},{"shortcodes":["orthodox_cross"],"annotation":"orthodox cross","tags":["christian","cross","religion"],"emoji":"☦️","order":4534,"group":8,"version":1},{"shortcodes":["star_and_crescent"],"annotation":"star and crescent","tags":["islam","muslim","religion"],"emoji":"☪️","order":4536,"group":8,"version":0.7},{"shortcodes":["peace","peace_symbol"],"annotation":"peace symbol","tags":["peace"],"emoji":"☮️","order":4538,"group":8,"version":1},{"shortcodes":["menorah"],"annotation":"menorah","tags":["candelabrum","candlestick","religion"],"emoji":"🕎","order":4539,"group":8,"version":1},{"shortcodes":["six_pointed_star"],"annotation":"dotted six-pointed star","tags":["fortune","star"],"emoji":"🔯","order":4540,"group":8,"version":0.6},{"shortcodes":["khanda"],"annotation":"khanda","tags":["religion","sikh"],"emoji":"🪯","order":4541,"group":8,"version":15},{"shortcodes":["aries"],"annotation":"Aries","tags":["aries","ram","zodiac"],"emoji":"♈️","order":4542,"group":8,"version":0.6},{"shortcodes":["taurus"],"annotation":"Taurus","tags":["bull","ox","taurus","zodiac"],"emoji":"♉️","order":4543,"group":8,"version":0.6},{"shortcodes":["gemini"],"annotation":"Gemini","tags":["gemini","twins","zodiac"],"emoji":"♊️","order":4544,"group":8,"version":0.6},{"shortcodes":["cancer"],"annotation":"Cancer","tags":["cancer","crab","zodiac"],"emoji":"♋️","order":4545,"group":8,"version":0.6},{"shortcodes":["leo"],"annotation":"Leo","tags":["leo","lion","zodiac"],"emoji":"♌️","order":4546,"group":8,"version":0.6},{"shortcodes":["virgo"],"annotation":"Virgo","tags":["virgo","zodiac"],"emoji":"♍️","order":4547,"group":8,"version":0.6},{"shortcodes":["libra"],"annotation":"Libra","tags":["balance","justice","libra","scales","zodiac"],"emoji":"♎️","order":4548,"group":8,"version":0.6},{"shortcodes":["scorpius"],"annotation":"Scorpio","tags":["scorpio","scorpion","scorpius","zodiac"],"emoji":"♏️","order":4549,"group":8,"version":0.6},{"shortcodes":["sagittarius"],"annotation":"Sagittarius","tags":["archer","sagittarius","zodiac"],"emoji":"♐️","order":4550,"group":8,"version":0.6},{"shortcodes":["capricorn"],"annotation":"Capricorn","tags":["capricorn","goat","zodiac"],"emoji":"♑️","order":4551,"group":8,"version":0.6},{"shortcodes":["aquarius"],"annotation":"Aquarius","tags":["aquarius","bearer","water","zodiac"],"emoji":"♒️","order":4552,"group":8,"version":0.6},{"shortcodes":["pisces"],"annotation":"Pisces","tags":["fish","pisces","zodiac"],"emoji":"♓️","order":4553,"group":8,"version":0.6},{"shortcodes":["ophiuchus"],"annotation":"Ophiuchus","tags":["bearer","ophiuchus","serpent","snake","zodiac"],"emoji":"⛎️","order":4554,"group":8,"version":0.6},{"shortcodes":["shuffle","twisted_rightwards_arrows"],"annotation":"shuffle tracks button","tags":["arrow","crossed"],"emoji":"🔀","order":4555,"group":8,"version":1},{"shortcodes":["repeat"],"annotation":"repeat button","tags":["arrow","clockwise","repeat"],"emoji":"🔁","order":4556,"group":8,"version":1},{"shortcodes":["repeat_one"],"annotation":"repeat single button","tags":["arrow","clockwise","once"],"emoji":"🔂","order":4557,"group":8,"version":1},{"shortcodes":["arrow_forward","play"],"annotation":"play button","tags":["arrow","play","right","triangle"],"emoji":"▶️","order":4559,"group":8,"version":0.6},{"shortcodes":["fast_forward"],"annotation":"fast-forward button","tags":["arrow","double","fast","forward"],"emoji":"⏩️","order":4560,"group":8,"version":0.6},{"shortcodes":["next_track"],"annotation":"next track button","tags":["arrow","next scene","next track","triangle"],"emoji":"⏭️","order":4562,"group":8,"version":0.7},{"shortcodes":["play_pause"],"annotation":"play or pause button","tags":["arrow","pause","play","right","triangle"],"emoji":"⏯️","order":4564,"group":8,"version":1},{"shortcodes":["arrow_backward","reverse"],"annotation":"reverse button","tags":["arrow","left","reverse","triangle"],"emoji":"◀️","order":4566,"group":8,"version":0.6},{"shortcodes":["fast_reverse","rewind"],"annotation":"fast reverse button","tags":["arrow","double","rewind"],"emoji":"⏪️","order":4567,"group":8,"version":0.6},{"shortcodes":["previous_track"],"annotation":"last track button","tags":["arrow","previous scene","previous track","triangle"],"emoji":"⏮️","order":4569,"group":8,"version":0.7},{"shortcodes":["arrow_up_small","up"],"annotation":"upwards button","tags":["arrow","button"],"emoji":"🔼","order":4570,"group":8,"version":0.6},{"shortcodes":["arrow_double_up","fast_up"],"annotation":"fast up button","tags":["arrow","double"],"emoji":"⏫️","order":4571,"group":8,"version":0.6},{"shortcodes":["arrow_down_small","down"],"annotation":"downwards button","tags":["arrow","button","down"],"emoji":"🔽","order":4572,"group":8,"version":0.6},{"shortcodes":["arrow_double_down","fast_down"],"annotation":"fast down button","tags":["arrow","double","down"],"emoji":"⏬️","order":4573,"group":8,"version":0.6},{"shortcodes":["pause"],"annotation":"pause button","tags":["bar","double","pause","vertical"],"emoji":"⏸️","order":4575,"group":8,"version":0.7},{"shortcodes":["stop"],"annotation":"stop button","tags":["square","stop"],"emoji":"⏹️","order":4577,"group":8,"version":0.7},{"shortcodes":["record"],"annotation":"record button","tags":["circle","record"],"emoji":"⏺️","order":4579,"group":8,"version":0.7},{"shortcodes":["eject"],"annotation":"eject button","tags":["eject"],"emoji":"⏏️","order":4581,"group":8,"version":1},{"shortcodes":["cinema"],"annotation":"cinema","tags":["camera","film","movie"],"emoji":"🎦","order":4582,"group":8,"version":0.6},{"shortcodes":["dim_button","low_brightness"],"annotation":"dim button","tags":["brightness","dim","low"],"emoji":"🔅","order":4583,"group":8,"version":1},{"shortcodes":["bright_button","high_brightness"],"annotation":"bright button","tags":["bright","brightness"],"emoji":"🔆","order":4584,"group":8,"version":1},{"shortcodes":["antenna_bars","signal_strength"],"annotation":"antenna bars","tags":["antenna","bar","cell","mobile","phone"],"emoji":"📶","order":4585,"group":8,"version":0.6},{"shortcodes":["wireless"],"annotation":"wireless","tags":["computer","internet","network","wi-fi","wifi"],"emoji":"🛜","order":4586,"group":8,"version":15},{"shortcodes":["vibration_mode"],"annotation":"vibration mode","tags":["cell","mobile","mode","phone","telephone","vibration"],"emoji":"📳","order":4587,"group":8,"version":0.6},{"shortcodes":["mobile_phone_off"],"annotation":"mobile phone off","tags":["cell","mobile","off","phone","telephone"],"emoji":"📴","order":4588,"group":8,"version":0.6},{"shortcodes":["female","female_sign"],"annotation":"female sign","tags":["woman"],"emoji":"♀️","order":4590,"group":8,"version":4},{"shortcodes":["male","male_sign"],"annotation":"male sign","tags":["man"],"emoji":"♂️","order":4592,"group":8,"version":4},{"shortcodes":["transgender_symbol"],"annotation":"transgender symbol","tags":["transgender"],"emoji":"⚧️","order":4594,"group":8,"version":13},{"shortcodes":["multiplication","multiply"],"annotation":"multiply","tags":["cancel","multiplication","sign","x","×"],"emoji":"✖️","order":4596,"group":8,"version":0.6},{"shortcodes":["plus"],"annotation":"plus","tags":["+","math","sign"],"emoji":"➕️","order":4597,"group":8,"version":0.6},{"shortcodes":["minus"],"annotation":"minus","tags":["-","math","sign","−"],"emoji":"➖️","order":4598,"group":8,"version":0.6},{"shortcodes":["divide","division"],"annotation":"divide","tags":["division","math","sign","÷"],"emoji":"➗️","order":4599,"group":8,"version":0.6},{"shortcodes":["heavy_equals_sign"],"annotation":"heavy equals sign","tags":["equality","math"],"emoji":"🟰","order":4600,"group":8,"version":14},{"shortcodes":["infinity"],"annotation":"infinity","tags":["forever","unbounded","universal"],"emoji":"♾️","order":4602,"group":8,"version":11},{"shortcodes":["bangbang","double_exclamation"],"annotation":"double exclamation mark","tags":["!","!!","bangbang","exclamation","mark"],"emoji":"‼️","order":4604,"group":8,"version":0.6},{"shortcodes":["exclamation_question","interrobang"],"annotation":"exclamation question mark","tags":["!","!?","?","exclamation","interrobang","mark","punctuation","question"],"emoji":"⁉️","order":4606,"group":8,"version":0.6},{"shortcodes":["question"],"annotation":"red question mark","tags":["?","mark","punctuation","question"],"emoji":"❓️","order":4607,"group":8,"version":0.6},{"shortcodes":["white_question"],"annotation":"white question mark","tags":["?","mark","outlined","punctuation","question"],"emoji":"❔️","order":4608,"group":8,"version":0.6},{"shortcodes":["white_exclamation"],"annotation":"white exclamation mark","tags":["!","exclamation","mark","outlined","punctuation"],"emoji":"❕️","order":4609,"group":8,"version":0.6},{"shortcodes":["exclamation"],"annotation":"red exclamation mark","tags":["!","exclamation","mark","punctuation"],"emoji":"❗️","order":4610,"group":8,"version":0.6},{"shortcodes":["wavy_dash"],"annotation":"wavy dash","tags":["dash","punctuation","wavy"],"emoji":"〰️","order":4612,"group":8,"version":0.6},{"shortcodes":["currency_exchange"],"annotation":"currency exchange","tags":["bank","currency","exchange","money"],"emoji":"💱","order":4613,"group":8,"version":0.6},{"shortcodes":["heavy_dollar_sign"],"annotation":"heavy dollar sign","tags":["currency","dollar","money"],"emoji":"💲","order":4614,"group":8,"version":0.6},{"shortcodes":["medical","medical_symbol"],"annotation":"medical symbol","tags":["aesculapius","medicine","staff"],"emoji":"⚕️","order":4616,"group":8,"version":4},{"shortcodes":["recycle","recycling_symbol"],"annotation":"recycling symbol","tags":["recycle"],"emoji":"♻️","order":4618,"group":8,"version":0.6},{"shortcodes":["fleur-de-lis"],"annotation":"fleur-de-lis","tags":["fleur-de-lis"],"emoji":"⚜️","order":4620,"group":8,"version":1},{"shortcodes":["trident"],"annotation":"trident emblem","tags":["anchor","emblem","ship","tool","trident"],"emoji":"🔱","order":4621,"group":8,"version":0.6},{"shortcodes":["name_badge"],"annotation":"name badge","tags":["badge","name"],"emoji":"📛","order":4622,"group":8,"version":0.6},{"shortcodes":["beginner"],"annotation":"Japanese symbol for beginner","tags":["beginner","chevron","japanese","japanese symbol for beginner","leaf"],"emoji":"🔰","order":4623,"group":8,"version":0.6},{"shortcodes":["hollow_red_circle","red_o"],"annotation":"hollow red circle","tags":["circle","large","o","red"],"emoji":"⭕️","order":4624,"group":8,"version":0.6},{"shortcodes":["check_mark_button","white_check_mark"],"annotation":"check mark button","tags":["button","check","mark","✓"],"emoji":"✅️","order":4625,"group":8,"version":0.6},{"shortcodes":["ballot_box_with_check"],"annotation":"check box with check","tags":["box","check","✓"],"emoji":"☑️","order":4627,"group":8,"version":0.6},{"shortcodes":["check_mark","heavy_check_mark"],"annotation":"check mark","tags":["check","mark","✓"],"emoji":"✔️","order":4629,"group":8,"version":0.6},{"shortcodes":["cross_mark","x"],"annotation":"cross mark","tags":["cancel","cross","mark","multiplication","multiply","x","×"],"emoji":"❌️","order":4630,"group":8,"version":0.6},{"shortcodes":["cross_mark_button","negative_squared_cross_mark"],"annotation":"cross mark button","tags":["mark","square","x","×"],"emoji":"❎️","order":4631,"group":8,"version":0.6},{"shortcodes":["curly_loop"],"annotation":"curly loop","tags":["curl","loop"],"emoji":"➰️","order":4632,"group":8,"version":0.6},{"shortcodes":["double_curly_loop","loop"],"annotation":"double curly loop","tags":["curl","double","loop"],"emoji":"➿️","order":4633,"group":8,"version":1},{"shortcodes":["part_alternation_mark"],"annotation":"part alternation mark","tags":["mark","part"],"emoji":"〽️","order":4635,"group":8,"version":0.6},{"shortcodes":["eight_spoked_asterisk"],"annotation":"eight-spoked asterisk","tags":["*","asterisk"],"emoji":"✳️","order":4637,"group":8,"version":0.6},{"shortcodes":["eight_pointed_black_star"],"annotation":"eight-pointed star","tags":["*","star"],"emoji":"✴️","order":4639,"group":8,"version":0.6},{"shortcodes":["sparkle"],"annotation":"sparkle","tags":["*"],"emoji":"❇️","order":4641,"group":8,"version":0.6},{"shortcodes":["copyright"],"annotation":"copyright","tags":["c"],"emoji":"©️","order":4643,"group":8,"version":0.6},{"shortcodes":["registered"],"annotation":"registered","tags":["r"],"emoji":"®️","order":4645,"group":8,"version":0.6},{"shortcodes":["tm","trade_mark"],"annotation":"trade mark","tags":["mark","tm","trademark"],"emoji":"™️","order":4647,"group":8,"version":0.6},{"shortcodes":["hash","number_sign"],"annotation":"keycap: #","tags":["keycap"],"emoji":"#️⃣","order":4648,"group":8,"version":0.6},{"shortcodes":["asterisk"],"annotation":"keycap: *","tags":["keycap"],"emoji":"*️⃣","order":4650,"group":8,"version":2},{"shortcodes":["zero"],"annotation":"keycap: 0","tags":["keycap"],"emoji":"0️⃣","order":4652,"group":8,"version":0.6},{"shortcodes":["one"],"annotation":"keycap: 1","tags":["keycap"],"emoji":"1️⃣","order":4654,"group":8,"version":0.6},{"shortcodes":["two"],"annotation":"keycap: 2","tags":["keycap"],"emoji":"2️⃣","order":4656,"group":8,"version":0.6},{"shortcodes":["three"],"annotation":"keycap: 3","tags":["keycap"],"emoji":"3️⃣","order":4658,"group":8,"version":0.6},{"shortcodes":["four"],"annotation":"keycap: 4","tags":["keycap"],"emoji":"4️⃣","order":4660,"group":8,"version":0.6},{"shortcodes":["five"],"annotation":"keycap: 5","tags":["keycap"],"emoji":"5️⃣","order":4662,"group":8,"version":0.6},{"shortcodes":["six"],"annotation":"keycap: 6","tags":["keycap"],"emoji":"6️⃣","order":4664,"group":8,"version":0.6},{"shortcodes":["seven"],"annotation":"keycap: 7","tags":["keycap"],"emoji":"7️⃣","order":4666,"group":8,"version":0.6},{"shortcodes":["eight"],"annotation":"keycap: 8","tags":["keycap"],"emoji":"8️⃣","order":4668,"group":8,"version":0.6},{"shortcodes":["nine"],"annotation":"keycap: 9","tags":["keycap"],"emoji":"9️⃣","order":4670,"group":8,"version":0.6},{"shortcodes":["ten"],"annotation":"keycap: 10","tags":["keycap"],"emoji":"🔟","order":4672,"group":8,"version":0.6},{"shortcodes":["capital_abcd"],"annotation":"input latin uppercase","tags":["abcd","input","latin","letters","uppercase"],"emoji":"🔠","order":4673,"group":8,"version":0.6},{"shortcodes":["abcd"],"annotation":"input latin lowercase","tags":["abcd","input","latin","letters","lowercase"],"emoji":"🔡","order":4674,"group":8,"version":0.6},{"shortcodes":["1234"],"annotation":"input numbers","tags":["1234","input","numbers"],"emoji":"🔢","order":4675,"group":8,"version":0.6},{"shortcodes":["symbols"],"annotation":"input symbols","tags":["input","〒♪&%"],"emoji":"🔣","order":4676,"group":8,"version":0.6},{"shortcodes":["abc"],"annotation":"input latin letters","tags":["abc","alphabet","input","latin","letters"],"emoji":"🔤","order":4677,"group":8,"version":0.6},{"shortcodes":["a","a_blood"],"annotation":"A button (blood type)","tags":["a","a button (blood type)","blood type"],"emoji":"🅰️","order":4679,"group":8,"version":0.6},{"shortcodes":["ab","ab_blood"],"annotation":"AB button (blood type)","tags":["ab","ab button (blood type)","blood type"],"emoji":"🆎","order":4680,"group":8,"version":0.6},{"shortcodes":["b","b_blood"],"annotation":"B button (blood type)","tags":["b","b button (blood type)","blood type"],"emoji":"🅱️","order":4682,"group":8,"version":0.6},{"shortcodes":["cl"],"annotation":"CL button","tags":["cl","cl button"],"emoji":"🆑","order":4683,"group":8,"version":0.6},{"shortcodes":["cool"],"annotation":"COOL button","tags":["cool","cool button"],"emoji":"🆒","order":4684,"group":8,"version":0.6},{"shortcodes":["free"],"annotation":"FREE button","tags":["free","free button"],"emoji":"🆓","order":4685,"group":8,"version":0.6},{"shortcodes":["info","information_source"],"annotation":"information","tags":["i"],"emoji":"ℹ️","order":4687,"group":8,"version":0.6},{"shortcodes":["id"],"annotation":"ID button","tags":["id","id button","identity"],"emoji":"🆔","order":4688,"group":8,"version":0.6},{"shortcodes":["m"],"annotation":"circled M","tags":["circle","circled m","m"],"emoji":"Ⓜ️","order":4690,"group":8,"version":0.6},{"shortcodes":["new"],"annotation":"NEW button","tags":["new","new button"],"emoji":"🆕","order":4691,"group":8,"version":0.6},{"shortcodes":["ng"],"annotation":"NG button","tags":["ng","ng button"],"emoji":"🆖","order":4692,"group":8,"version":0.6},{"shortcodes":["o","o_blood"],"annotation":"O button (blood type)","tags":["blood type","o","o button (blood type)"],"emoji":"🅾️","order":4694,"group":8,"version":0.6},{"shortcodes":["ok"],"annotation":"OK button","tags":["ok","ok button"],"emoji":"🆗","order":4695,"group":8,"version":0.6},{"shortcodes":["parking"],"annotation":"P button","tags":["p","p button","parking"],"emoji":"🅿️","order":4697,"group":8,"version":0.6},{"shortcodes":["sos"],"annotation":"SOS button","tags":["help","sos","sos button"],"emoji":"🆘","order":4698,"group":8,"version":0.6},{"shortcodes":["up2"],"annotation":"UP! button","tags":["mark","up","up!","up! button"],"emoji":"🆙","order":4699,"group":8,"version":0.6},{"shortcodes":["vs"],"annotation":"VS button","tags":["versus","vs","vs button"],"emoji":"🆚","order":4700,"group":8,"version":0.6},{"shortcodes":["ja_here","koko"],"annotation":"Japanese “here” button","tags":["japanese","japanese “here” button","katakana","“here”","ココ"],"emoji":"🈁","order":4701,"group":8,"version":0.6},{"shortcodes":["ja_service_charge"],"annotation":"Japanese “service charge” button","tags":["japanese","japanese “service charge” button","katakana","“service charge”","サ"],"emoji":"🈂️","order":4703,"group":8,"version":0.6},{"shortcodes":["ja_monthly_amount"],"annotation":"Japanese “monthly amount” button","tags":["ideograph","japanese","japanese “monthly amount” button","“monthly amount”","月"],"emoji":"🈷️","order":4705,"group":8,"version":0.6},{"shortcodes":["ja_not_free_of_carge"],"annotation":"Japanese “not free of charge” button","tags":["ideograph","japanese","japanese “not free of charge” button","“not free of charge”","有"],"emoji":"🈶","order":4706,"group":8,"version":0.6},{"shortcodes":["ja_reserved"],"annotation":"Japanese “reserved” button","tags":["ideograph","japanese","japanese “reserved” button","“reserved”","指"],"emoji":"🈯️","order":4707,"group":8,"version":0.6},{"shortcodes":["ideograph_advantage","ja_bargain"],"annotation":"Japanese “bargain” button","tags":["ideograph","japanese","japanese “bargain” button","“bargain”","得"],"emoji":"🉐","order":4708,"group":8,"version":0.6},{"shortcodes":["ja_discount"],"annotation":"Japanese “discount” button","tags":["ideograph","japanese","japanese “discount” button","“discount”","割"],"emoji":"🈹","order":4709,"group":8,"version":0.6},{"shortcodes":["ja_free_of_charge"],"annotation":"Japanese “free of charge” button","tags":["ideograph","japanese","japanese “free of charge” button","“free of charge”","無"],"emoji":"🈚️","order":4710,"group":8,"version":0.6},{"shortcodes":["ja_prohibited"],"annotation":"Japanese “prohibited” button","tags":["ideograph","japanese","japanese “prohibited” button","“prohibited”","禁"],"emoji":"🈲","order":4711,"group":8,"version":0.6},{"shortcodes":["accept","ja_acceptable"],"annotation":"Japanese “acceptable” button","tags":["ideograph","japanese","japanese “acceptable” button","“acceptable”","可"],"emoji":"🉑","order":4712,"group":8,"version":0.6},{"shortcodes":["ja_application"],"annotation":"Japanese “application” button","tags":["ideograph","japanese","japanese “application” button","“application”","申"],"emoji":"🈸","order":4713,"group":8,"version":0.6},{"shortcodes":["ja_passing_grade"],"annotation":"Japanese “passing grade” button","tags":["ideograph","japanese","japanese “passing grade” button","“passing grade”","合"],"emoji":"🈴","order":4714,"group":8,"version":0.6},{"shortcodes":["ja_vacancy"],"annotation":"Japanese “vacancy” button","tags":["ideograph","japanese","japanese “vacancy” button","“vacancy”","空"],"emoji":"🈳","order":4715,"group":8,"version":0.6},{"shortcodes":["congratulations","ja_congratulations"],"annotation":"Japanese “congratulations” button","tags":["ideograph","japanese","japanese “congratulations” button","“congratulations”","祝"],"emoji":"㊗️","order":4717,"group":8,"version":0.6},{"shortcodes":["ja_secret","secret"],"annotation":"Japanese “secret” button","tags":["ideograph","japanese","japanese “secret” button","“secret”","秘"],"emoji":"㊙️","order":4719,"group":8,"version":0.6},{"shortcodes":["ja_open_for_business"],"annotation":"Japanese “open for business” button","tags":["ideograph","japanese","japanese “open for business” button","“open for business”","営"],"emoji":"🈺","order":4720,"group":8,"version":0.6},{"shortcodes":["ja_no_vacancy"],"annotation":"Japanese “no vacancy” button","tags":["ideograph","japanese","japanese “no vacancy” button","“no vacancy”","満"],"emoji":"🈵","order":4721,"group":8,"version":0.6},{"shortcodes":["red_circle"],"annotation":"red circle","tags":["circle","geometric","red"],"emoji":"🔴","order":4722,"group":8,"version":0.6},{"shortcodes":["orange_circle"],"annotation":"orange circle","tags":["circle","orange"],"emoji":"🟠","order":4723,"group":8,"version":12},{"shortcodes":["yellow_circle"],"annotation":"yellow circle","tags":["circle","yellow"],"emoji":"🟡","order":4724,"group":8,"version":12},{"shortcodes":["green_circle"],"annotation":"green circle","tags":["circle","green"],"emoji":"🟢","order":4725,"group":8,"version":12},{"shortcodes":["blue_circle"],"annotation":"blue circle","tags":["blue","circle","geometric"],"emoji":"🔵","order":4726,"group":8,"version":0.6},{"shortcodes":["purple_circle"],"annotation":"purple circle","tags":["circle","purple"],"emoji":"🟣","order":4727,"group":8,"version":12},{"shortcodes":["brown_circle"],"annotation":"brown circle","tags":["brown","circle"],"emoji":"🟤","order":4728,"group":8,"version":12},{"shortcodes":["black_circle"],"annotation":"black circle","tags":["circle","geometric"],"emoji":"⚫️","order":4729,"group":8,"version":0.6},{"shortcodes":["white_circle"],"annotation":"white circle","tags":["circle","geometric"],"emoji":"⚪️","order":4730,"group":8,"version":0.6},{"shortcodes":["red_square"],"annotation":"red square","tags":["red","square"],"emoji":"🟥","order":4731,"group":8,"version":12},{"shortcodes":["orange_square"],"annotation":"orange square","tags":["orange","square"],"emoji":"🟧","order":4732,"group":8,"version":12},{"shortcodes":["yellow_square"],"annotation":"yellow square","tags":["square","yellow"],"emoji":"🟨","order":4733,"group":8,"version":12},{"shortcodes":["green_square"],"annotation":"green square","tags":["green","square"],"emoji":"🟩","order":4734,"group":8,"version":12},{"shortcodes":["blue_square"],"annotation":"blue square","tags":["blue","square"],"emoji":"🟦","order":4735,"group":8,"version":12},{"shortcodes":["purple_square"],"annotation":"purple square","tags":["purple","square"],"emoji":"🟪","order":4736,"group":8,"version":12},{"shortcodes":["brown_square"],"annotation":"brown square","tags":["brown","square"],"emoji":"🟫","order":4737,"group":8,"version":12},{"shortcodes":["black_large_square"],"annotation":"black large square","tags":["geometric","square"],"emoji":"⬛️","order":4738,"group":8,"version":0.6},{"shortcodes":["white_large_square"],"annotation":"white large square","tags":["geometric","square"],"emoji":"⬜️","order":4739,"group":8,"version":0.6},{"shortcodes":["black_medium_square"],"annotation":"black medium square","tags":["geometric","square"],"emoji":"◼️","order":4741,"group":8,"version":0.6},{"shortcodes":["white_medium_square"],"annotation":"white medium square","tags":["geometric","square"],"emoji":"◻️","order":4743,"group":8,"version":0.6},{"shortcodes":["black_medium_small_square"],"annotation":"black medium-small square","tags":["geometric","square"],"emoji":"◾️","order":4744,"group":8,"version":0.6},{"shortcodes":["white_medium_small_square"],"annotation":"white medium-small square","tags":["geometric","square"],"emoji":"◽️","order":4745,"group":8,"version":0.6},{"shortcodes":["black_small_square"],"annotation":"black small square","tags":["geometric","square"],"emoji":"▪️","order":4747,"group":8,"version":0.6},{"shortcodes":["white_small_square"],"annotation":"white small square","tags":["geometric","square"],"emoji":"▫️","order":4749,"group":8,"version":0.6},{"shortcodes":["large_orange_diamond"],"annotation":"large orange diamond","tags":["diamond","geometric","orange"],"emoji":"🔶","order":4750,"group":8,"version":0.6},{"shortcodes":["large_blue_diamond"],"annotation":"large blue diamond","tags":["blue","diamond","geometric"],"emoji":"🔷","order":4751,"group":8,"version":0.6},{"shortcodes":["small_orange_diamond"],"annotation":"small orange diamond","tags":["diamond","geometric","orange"],"emoji":"🔸","order":4752,"group":8,"version":0.6},{"shortcodes":["small_blue_diamond"],"annotation":"small blue diamond","tags":["blue","diamond","geometric"],"emoji":"🔹","order":4753,"group":8,"version":0.6},{"shortcodes":["small_red_triangle"],"annotation":"red triangle pointed up","tags":["geometric","red"],"emoji":"🔺","order":4754,"group":8,"version":0.6},{"shortcodes":["small_red_triangle_down"],"annotation":"red triangle pointed down","tags":["down","geometric","red"],"emoji":"🔻","order":4755,"group":8,"version":0.6},{"shortcodes":["diamond_shape_with_a_dot_inside","diamond_with_a_dot"],"annotation":"diamond with a dot","tags":["comic","diamond","geometric","inside"],"emoji":"💠","order":4756,"group":8,"version":0.6},{"shortcodes":["radio_button"],"annotation":"radio button","tags":["button","geometric","radio"],"emoji":"🔘","order":4757,"group":8,"version":0.6},{"shortcodes":["white_square_button"],"annotation":"white square button","tags":["button","geometric","outlined","square"],"emoji":"🔳","order":4758,"group":8,"version":0.6},{"shortcodes":["black_square_button"],"annotation":"black square button","tags":["button","geometric","square"],"emoji":"🔲","order":4759,"group":8,"version":0.6},{"shortcodes":["checkered_flag"],"annotation":"chequered flag","tags":["checkered","chequered","racing"],"emoji":"🏁","order":4760,"group":9,"version":0.6},{"shortcodes":["triangular_flag","triangular_flag_on_post"],"annotation":"triangular flag","tags":["post"],"emoji":"🚩","order":4761,"group":9,"version":0.6},{"shortcodes":["crossed_flags"],"annotation":"crossed flags","tags":["celebration","cross","crossed","japanese"],"emoji":"🎌","order":4762,"group":9,"version":0.6},{"shortcodes":["black_flag"],"annotation":"black flag","tags":["waving"],"emoji":"🏴","order":4763,"group":9,"version":1},{"shortcodes":["white_flag"],"annotation":"white flag","tags":["waving"],"emoji":"🏳️","order":4765,"group":9,"version":0.7},{"shortcodes":["rainbow_flag"],"annotation":"rainbow flag","tags":["pride","rainbow"],"emoji":"🏳️‍🌈","order":4766,"group":9,"version":4},{"shortcodes":["transgender_flag"],"annotation":"transgender flag","tags":["flag","light blue","pink","transgender","white"],"emoji":"🏳️‍⚧️","order":4768,"group":9,"version":13},{"shortcodes":["jolly_roger","pirate_flag"],"annotation":"pirate flag","tags":["jolly roger","pirate","plunder","treasure"],"emoji":"🏴‍☠️","order":4772,"group":9,"version":11},{"shortcodes":["ascension_island","flag_ac"],"annotation":"flag: Ascension Island","tags":["AC","flag","flag: ascension island"],"emoji":"🇦🇨","order":4774,"group":9,"version":2},{"shortcodes":["andorra","flag_ad"],"annotation":"flag: Andorra","tags":["AD","flag","flag: andorra"],"emoji":"🇦🇩","order":4775,"group":9,"version":2},{"shortcodes":["flag_ae","united_arab_emirates"],"annotation":"flag: United Arab Emirates","tags":["AE","flag","flag: united arab emirates"],"emoji":"🇦🇪","order":4776,"group":9,"version":2},{"shortcodes":["afghanistan","flag_af"],"annotation":"flag: Afghanistan","tags":["AF","flag","flag: afghanistan"],"emoji":"🇦🇫","order":4777,"group":9,"version":2},{"shortcodes":["antigua_barbuda","flag_ag"],"annotation":"flag: Antigua & Barbuda","tags":["AG","flag","flag: antigua & barbuda"],"emoji":"🇦🇬","order":4778,"group":9,"version":2},{"shortcodes":["anguilla","flag_ai"],"annotation":"flag: Anguilla","tags":["AI","flag","flag: anguilla"],"emoji":"🇦🇮","order":4779,"group":9,"version":2},{"shortcodes":["albania","flag_al"],"annotation":"flag: Albania","tags":["AL","flag","flag: albania"],"emoji":"🇦🇱","order":4780,"group":9,"version":2},{"shortcodes":["armenia","flag_am"],"annotation":"flag: Armenia","tags":["AM","flag","flag: armenia"],"emoji":"🇦🇲","order":4781,"group":9,"version":2},{"shortcodes":["angola","flag_ao"],"annotation":"flag: Angola","tags":["AO","flag","flag: angola"],"emoji":"🇦🇴","order":4782,"group":9,"version":2},{"shortcodes":["antarctica","flag_aq"],"annotation":"flag: Antarctica","tags":["AQ","flag","flag: antarctica"],"emoji":"🇦🇶","order":4783,"group":9,"version":2},{"shortcodes":["argentina","flag_ar"],"annotation":"flag: Argentina","tags":["AR","flag","flag: argentina"],"emoji":"🇦🇷","order":4784,"group":9,"version":2},{"shortcodes":["american_samoa","flag_as"],"annotation":"flag: American Samoa","tags":["AS","flag","flag: american samoa"],"emoji":"🇦🇸","order":4785,"group":9,"version":2},{"shortcodes":["austria","flag_at"],"annotation":"flag: Austria","tags":["AT","flag","flag: austria"],"emoji":"🇦🇹","order":4786,"group":9,"version":2},{"shortcodes":["australia","flag_au"],"annotation":"flag: Australia","tags":["AU","flag","flag: australia"],"emoji":"🇦🇺","order":4787,"group":9,"version":2},{"shortcodes":["aruba","flag_aw"],"annotation":"flag: Aruba","tags":["AW","flag","flag: aruba"],"emoji":"🇦🇼","order":4788,"group":9,"version":2},{"shortcodes":["aland_islands","flag_ax"],"annotation":"flag: Åland Islands","tags":["AX","flag","flag: åland islands"],"emoji":"🇦🇽","order":4789,"group":9,"version":2},{"shortcodes":["azerbaijan","flag_az"],"annotation":"flag: Azerbaijan","tags":["AZ","flag","flag: azerbaijan"],"emoji":"🇦🇿","order":4790,"group":9,"version":2},{"shortcodes":["bosnia_herzegovina","flag_ba"],"annotation":"flag: Bosnia & Herzegovina","tags":["BA","flag","flag: bosnia & herzegovina"],"emoji":"🇧🇦","order":4791,"group":9,"version":2},{"shortcodes":["barbados","flag_bb"],"annotation":"flag: Barbados","tags":["BB","flag","flag: barbados"],"emoji":"🇧🇧","order":4792,"group":9,"version":2},{"shortcodes":["bangladesh","flag_bd"],"annotation":"flag: Bangladesh","tags":["BD","flag","flag: bangladesh"],"emoji":"🇧🇩","order":4793,"group":9,"version":2},{"shortcodes":["belgium","flag_be"],"annotation":"flag: Belgium","tags":["BE","flag","flag: belgium"],"emoji":"🇧🇪","order":4794,"group":9,"version":2},{"shortcodes":["burkina_faso","flag_bf"],"annotation":"flag: Burkina Faso","tags":["BF","flag","flag: burkina faso"],"emoji":"🇧🇫","order":4795,"group":9,"version":2},{"shortcodes":["bulgaria","flag_bg"],"annotation":"flag: Bulgaria","tags":["BG","flag","flag: bulgaria"],"emoji":"🇧🇬","order":4796,"group":9,"version":2},{"shortcodes":["bahrain","flag_bh"],"annotation":"flag: Bahrain","tags":["BH","flag","flag: bahrain"],"emoji":"🇧🇭","order":4797,"group":9,"version":2},{"shortcodes":["burundi","flag_bi"],"annotation":"flag: Burundi","tags":["BI","flag","flag: burundi"],"emoji":"🇧🇮","order":4798,"group":9,"version":2},{"shortcodes":["benin","flag_bj"],"annotation":"flag: Benin","tags":["BJ","flag","flag: benin"],"emoji":"🇧🇯","order":4799,"group":9,"version":2},{"shortcodes":["flag_bl","st_barthelemy"],"annotation":"flag: St. Barthélemy","tags":["BL","flag","flag: st. barthélemy"],"emoji":"🇧🇱","order":4800,"group":9,"version":2},{"shortcodes":["bermuda","flag_bm"],"annotation":"flag: Bermuda","tags":["BM","flag","flag: bermuda"],"emoji":"🇧🇲","order":4801,"group":9,"version":2},{"shortcodes":["brunei","flag_bn"],"annotation":"flag: Brunei","tags":["BN","flag","flag: brunei"],"emoji":"🇧🇳","order":4802,"group":9,"version":2},{"shortcodes":["bolivia","flag_bo"],"annotation":"flag: Bolivia","tags":["BO","flag","flag: bolivia"],"emoji":"🇧🇴","order":4803,"group":9,"version":2},{"shortcodes":["caribbean_netherlands","flag_bq"],"annotation":"flag: Caribbean Netherlands","tags":["BQ","flag","flag: caribbean netherlands"],"emoji":"🇧🇶","order":4804,"group":9,"version":2},{"shortcodes":["brazil","flag_br"],"annotation":"flag: Brazil","tags":["BR","flag","flag: brazil"],"emoji":"🇧🇷","order":4805,"group":9,"version":2},{"shortcodes":["bahamas","flag_bs"],"annotation":"flag: Bahamas","tags":["BS","flag","flag: bahamas"],"emoji":"🇧🇸","order":4806,"group":9,"version":2},{"shortcodes":["bhutan","flag_bt"],"annotation":"flag: Bhutan","tags":["BT","flag","flag: bhutan"],"emoji":"🇧🇹","order":4807,"group":9,"version":2},{"shortcodes":["bouvet_island","flag_bv"],"annotation":"flag: Bouvet Island","tags":["BV","flag","flag: bouvet island"],"emoji":"🇧🇻","order":4808,"group":9,"version":2},{"shortcodes":["botswana","flag_bw"],"annotation":"flag: Botswana","tags":["BW","flag","flag: botswana"],"emoji":"🇧🇼","order":4809,"group":9,"version":2},{"shortcodes":["belarus","flag_by"],"annotation":"flag: Belarus","tags":["BY","flag","flag: belarus"],"emoji":"🇧🇾","order":4810,"group":9,"version":2},{"shortcodes":["belize","flag_bz"],"annotation":"flag: Belize","tags":["BZ","flag","flag: belize"],"emoji":"🇧🇿","order":4811,"group":9,"version":2},{"shortcodes":["canada","flag_ca"],"annotation":"flag: Canada","tags":["CA","flag","flag: canada"],"emoji":"🇨🇦","order":4812,"group":9,"version":2},{"shortcodes":["cocos_islands","flag_cc"],"annotation":"flag: Cocos (Keeling) Islands","tags":["CC","flag","flag: cocos (keeling) islands"],"emoji":"🇨🇨","order":4813,"group":9,"version":2},{"shortcodes":["congo_kinshasa","flag_cd"],"annotation":"flag: Congo - Kinshasa","tags":["CD","flag","flag: congo - kinshasa"],"emoji":"🇨🇩","order":4814,"group":9,"version":2},{"shortcodes":["central_african_republic","flag_cf"],"annotation":"flag: Central African Republic","tags":["CF","flag","flag: central african republic"],"emoji":"🇨🇫","order":4815,"group":9,"version":2},{"shortcodes":["congo_brazzaville","flag_cg"],"annotation":"flag: Congo - Brazzaville","tags":["CG","flag","flag: congo - brazzaville"],"emoji":"🇨🇬","order":4816,"group":9,"version":2},{"shortcodes":["flag_ch","switzerland"],"annotation":"flag: Switzerland","tags":["CH","flag","flag: switzerland"],"emoji":"🇨🇭","order":4817,"group":9,"version":2},{"shortcodes":["cote_divoire","flag_ci"],"annotation":"flag: Côte d’Ivoire","tags":["CI","flag","flag: côte d’ivoire"],"emoji":"🇨🇮","order":4818,"group":9,"version":2},{"shortcodes":["cook_islands","flag_ck"],"annotation":"flag: Cook Islands","tags":["CK","flag","flag: cook islands"],"emoji":"🇨🇰","order":4819,"group":9,"version":2},{"shortcodes":["chile","flag_cl"],"annotation":"flag: Chile","tags":["CL","flag","flag: chile"],"emoji":"🇨🇱","order":4820,"group":9,"version":2},{"shortcodes":["cameroon","flag_cm"],"annotation":"flag: Cameroon","tags":["CM","flag","flag: cameroon"],"emoji":"🇨🇲","order":4821,"group":9,"version":2},{"shortcodes":["china","flag_cn"],"annotation":"flag: China","tags":["CN","flag","flag: china"],"emoji":"🇨🇳","order":4822,"group":9,"version":0.6},{"shortcodes":["colombia","flag_co"],"annotation":"flag: Colombia","tags":["CO","flag","flag: colombia"],"emoji":"🇨🇴","order":4823,"group":9,"version":2},{"shortcodes":["clipperton_island","flag_cp"],"annotation":"flag: Clipperton Island","tags":["CP","flag","flag: clipperton island"],"emoji":"🇨🇵","order":4824,"group":9,"version":2},{"shortcodes":["costa_rica","flag_cr"],"annotation":"flag: Costa Rica","tags":["CR","flag","flag: costa rica"],"emoji":"🇨🇷","order":4825,"group":9,"version":2},{"shortcodes":["cuba","flag_cu"],"annotation":"flag: Cuba","tags":["CU","flag","flag: cuba"],"emoji":"🇨🇺","order":4826,"group":9,"version":2},{"shortcodes":["cape_verde","flag_cv"],"annotation":"flag: Cape Verde","tags":["CV","flag","flag: cape verde"],"emoji":"🇨🇻","order":4827,"group":9,"version":2},{"shortcodes":["curacao","flag_cw"],"annotation":"flag: Curaçao","tags":["CW","flag","flag: curaçao"],"emoji":"🇨🇼","order":4828,"group":9,"version":2},{"shortcodes":["christmas_island","flag_cx"],"annotation":"flag: Christmas Island","tags":["CX","flag","flag: christmas island"],"emoji":"🇨🇽","order":4829,"group":9,"version":2},{"shortcodes":["cyprus","flag_cy"],"annotation":"flag: Cyprus","tags":["CY","flag","flag: cyprus"],"emoji":"🇨🇾","order":4830,"group":9,"version":2},{"shortcodes":["czech_republic","czechia","flag_cz"],"annotation":"flag: Czechia","tags":["CZ","flag","flag: czechia"],"emoji":"🇨🇿","order":4831,"group":9,"version":2},{"shortcodes":["flag_de","germany"],"annotation":"flag: Germany","tags":["DE","flag","flag: germany"],"emoji":"🇩🇪","order":4832,"group":9,"version":0.6},{"shortcodes":["diego_garcia","flag_dg"],"annotation":"flag: Diego Garcia","tags":["DG","flag","flag: diego garcia"],"emoji":"🇩🇬","order":4833,"group":9,"version":2},{"shortcodes":["djibouti","flag_dj"],"annotation":"flag: Djibouti","tags":["DJ","flag","flag: djibouti"],"emoji":"🇩🇯","order":4834,"group":9,"version":2},{"shortcodes":["denmark","flag_dk"],"annotation":"flag: Denmark","tags":["DK","flag","flag: denmark"],"emoji":"🇩🇰","order":4835,"group":9,"version":2},{"shortcodes":["dominica","flag_dm"],"annotation":"flag: Dominica","tags":["DM","flag","flag: dominica"],"emoji":"🇩🇲","order":4836,"group":9,"version":2},{"shortcodes":["dominican_republic","flag_do"],"annotation":"flag: Dominican Republic","tags":["DO","flag","flag: dominican republic"],"emoji":"🇩🇴","order":4837,"group":9,"version":2},{"shortcodes":["algeria","flag_dz"],"annotation":"flag: Algeria","tags":["DZ","flag","flag: algeria"],"emoji":"🇩🇿","order":4838,"group":9,"version":2},{"shortcodes":["ceuta_melilla","flag_ea"],"annotation":"flag: Ceuta & Melilla","tags":["EA","flag","flag: ceuta & melilla"],"emoji":"🇪🇦","order":4839,"group":9,"version":2},{"shortcodes":["ecuador","flag_ec"],"annotation":"flag: Ecuador","tags":["EC","flag","flag: ecuador"],"emoji":"🇪🇨","order":4840,"group":9,"version":2},{"shortcodes":["estonia","flag_ee"],"annotation":"flag: Estonia","tags":["EE","flag","flag: estonia"],"emoji":"🇪🇪","order":4841,"group":9,"version":2},{"shortcodes":["egypt","flag_eg"],"annotation":"flag: Egypt","tags":["EG","flag","flag: egypt"],"emoji":"🇪🇬","order":4842,"group":9,"version":2},{"shortcodes":["flag_eh","western_sahara"],"annotation":"flag: Western Sahara","tags":["EH","flag","flag: western sahara"],"emoji":"🇪🇭","order":4843,"group":9,"version":2},{"shortcodes":["eritrea","flag_er"],"annotation":"flag: Eritrea","tags":["ER","flag","flag: eritrea"],"emoji":"🇪🇷","order":4844,"group":9,"version":2},{"shortcodes":["flag_es","spain"],"annotation":"flag: Spain","tags":["ES","flag","flag: spain"],"emoji":"🇪🇸","order":4845,"group":9,"version":0.6},{"shortcodes":["ethiopia","flag_et"],"annotation":"flag: Ethiopia","tags":["ET","flag","flag: ethiopia"],"emoji":"🇪🇹","order":4846,"group":9,"version":2},{"shortcodes":["european_union","flag_eu"],"annotation":"flag: European Union","tags":["EU","flag","flag: european union"],"emoji":"🇪🇺","order":4847,"group":9,"version":2},{"shortcodes":["finland","flag_fi"],"annotation":"flag: Finland","tags":["FI","flag","flag: finland"],"emoji":"🇫🇮","order":4848,"group":9,"version":2},{"shortcodes":["fiji","flag_fj"],"annotation":"flag: Fiji","tags":["FJ","flag","flag: fiji"],"emoji":"🇫🇯","order":4849,"group":9,"version":2},{"shortcodes":["falkland_islands","flag_fk"],"annotation":"flag: Falkland Islands","tags":["FK","flag","flag: falkland islands"],"emoji":"🇫🇰","order":4850,"group":9,"version":2},{"shortcodes":["flag_fm","micronesia"],"annotation":"flag: Micronesia","tags":["FM","flag","flag: micronesia"],"emoji":"🇫🇲","order":4851,"group":9,"version":2},{"shortcodes":["faroe_islands","flag_fo"],"annotation":"flag: Faroe Islands","tags":["FO","flag","flag: faroe islands"],"emoji":"🇫🇴","order":4852,"group":9,"version":2},{"shortcodes":["flag_fr","france"],"annotation":"flag: France","tags":["FR","flag","flag: france"],"emoji":"🇫🇷","order":4853,"group":9,"version":0.6},{"shortcodes":["flag_ga","gabon"],"annotation":"flag: Gabon","tags":["GA","flag","flag: gabon"],"emoji":"🇬🇦","order":4854,"group":9,"version":2},{"shortcodes":["flag_gb","uk","united_kingdom"],"annotation":"flag: United Kingdom","tags":["GB","flag","flag: united kingdom"],"emoji":"🇬🇧","order":4855,"group":9,"version":0.6},{"shortcodes":["flag_gd","grenada"],"annotation":"flag: Grenada","tags":["GD","flag","flag: grenada"],"emoji":"🇬🇩","order":4856,"group":9,"version":2},{"shortcodes":["flag_ge","georgia"],"annotation":"flag: Georgia","tags":["GE","flag","flag: georgia"],"emoji":"🇬🇪","order":4857,"group":9,"version":2},{"shortcodes":["flag_gf","french_guiana"],"annotation":"flag: French Guiana","tags":["GF","flag","flag: french guiana"],"emoji":"🇬🇫","order":4858,"group":9,"version":2},{"shortcodes":["flag_gg","guernsey"],"annotation":"flag: Guernsey","tags":["GG","flag","flag: guernsey"],"emoji":"🇬🇬","order":4859,"group":9,"version":2},{"shortcodes":["flag_gh","ghana"],"annotation":"flag: Ghana","tags":["GH","flag","flag: ghana"],"emoji":"🇬🇭","order":4860,"group":9,"version":2},{"shortcodes":["flag_gi","gibraltar"],"annotation":"flag: Gibraltar","tags":["GI","flag","flag: gibraltar"],"emoji":"🇬🇮","order":4861,"group":9,"version":2},{"shortcodes":["flag_gl","greenland"],"annotation":"flag: Greenland","tags":["GL","flag","flag: greenland"],"emoji":"🇬🇱","order":4862,"group":9,"version":2},{"shortcodes":["flag_gm","gambia"],"annotation":"flag: Gambia","tags":["GM","flag","flag: gambia"],"emoji":"🇬🇲","order":4863,"group":9,"version":2},{"shortcodes":["flag_gn","guinea"],"annotation":"flag: Guinea","tags":["GN","flag","flag: guinea"],"emoji":"🇬🇳","order":4864,"group":9,"version":2},{"shortcodes":["flag_gp","guadeloupe"],"annotation":"flag: Guadeloupe","tags":["GP","flag","flag: guadeloupe"],"emoji":"🇬🇵","order":4865,"group":9,"version":2},{"shortcodes":["equatorial_guinea","flag_gq"],"annotation":"flag: Equatorial Guinea","tags":["GQ","flag","flag: equatorial guinea"],"emoji":"🇬🇶","order":4866,"group":9,"version":2},{"shortcodes":["flag_gr","greece"],"annotation":"flag: Greece","tags":["GR","flag","flag: greece"],"emoji":"🇬🇷","order":4867,"group":9,"version":2},{"shortcodes":["flag_gs","south_georgia_south_sandwich_islands"],"annotation":"flag: South Georgia & South Sandwich Islands","tags":["GS","flag","flag: south georgia & south sandwich islands"],"emoji":"🇬🇸","order":4868,"group":9,"version":2},{"shortcodes":["flag_gt","guatemala"],"annotation":"flag: Guatemala","tags":["GT","flag","flag: guatemala"],"emoji":"🇬🇹","order":4869,"group":9,"version":2},{"shortcodes":["flag_gu","guam"],"annotation":"flag: Guam","tags":["GU","flag","flag: guam"],"emoji":"🇬🇺","order":4870,"group":9,"version":2},{"shortcodes":["flag_gw","guinea_bissau"],"annotation":"flag: Guinea-Bissau","tags":["GW","flag","flag: guinea-bissau"],"emoji":"🇬🇼","order":4871,"group":9,"version":2},{"shortcodes":["flag_gy","guyana"],"annotation":"flag: Guyana","tags":["GY","flag","flag: guyana"],"emoji":"🇬🇾","order":4872,"group":9,"version":2},{"shortcodes":["flag_hk","hong_kong"],"annotation":"flag: Hong Kong SAR China","tags":["HK","flag","flag: hong kong sar china"],"emoji":"🇭🇰","order":4873,"group":9,"version":2},{"shortcodes":["flag_hm","heard_mcdonald_islands"],"annotation":"flag: Heard & McDonald Islands","tags":["HM","flag","flag: heard & mcdonald islands"],"emoji":"🇭🇲","order":4874,"group":9,"version":2},{"shortcodes":["flag_hn","honduras"],"annotation":"flag: Honduras","tags":["HN","flag","flag: honduras"],"emoji":"🇭🇳","order":4875,"group":9,"version":2},{"shortcodes":["croatia","flag_hr"],"annotation":"flag: Croatia","tags":["HR","flag","flag: croatia"],"emoji":"🇭🇷","order":4876,"group":9,"version":2},{"shortcodes":["flag_ht","haiti"],"annotation":"flag: Haiti","tags":["HT","flag","flag: haiti"],"emoji":"🇭🇹","order":4877,"group":9,"version":2},{"shortcodes":["flag_hu","hungary"],"annotation":"flag: Hungary","tags":["HU","flag","flag: hungary"],"emoji":"🇭🇺","order":4878,"group":9,"version":2},{"shortcodes":["canary_islands","flag_ic"],"annotation":"flag: Canary Islands","tags":["IC","flag","flag: canary islands"],"emoji":"🇮🇨","order":4879,"group":9,"version":2},{"shortcodes":["flag_id","indonesia"],"annotation":"flag: Indonesia","tags":["ID","flag","flag: indonesia"],"emoji":"🇮🇩","order":4880,"group":9,"version":2},{"shortcodes":["flag_ie","ireland"],"annotation":"flag: Ireland","tags":["IE","flag","flag: ireland"],"emoji":"🇮🇪","order":4881,"group":9,"version":2},{"shortcodes":["flag_il","israel"],"annotation":"flag: Israel","tags":["IL","flag","flag: israel"],"emoji":"🇮🇱","order":4882,"group":9,"version":2},{"shortcodes":["flag_im","isle_of_man"],"annotation":"flag: Isle of Man","tags":["IM","flag","flag: isle of man"],"emoji":"🇮🇲","order":4883,"group":9,"version":2},{"shortcodes":["flag_in","india"],"annotation":"flag: India","tags":["IN","flag","flag: india"],"emoji":"🇮🇳","order":4884,"group":9,"version":2},{"shortcodes":["british_indian_ocean_territory","flag_io"],"annotation":"flag: British Indian Ocean Territory","tags":["IO","flag","flag: british indian ocean territory"],"emoji":"🇮🇴","order":4885,"group":9,"version":2},{"shortcodes":["flag_iq","iraq"],"annotation":"flag: Iraq","tags":["IQ","flag","flag: iraq"],"emoji":"🇮🇶","order":4886,"group":9,"version":2},{"shortcodes":["flag_ir","iran"],"annotation":"flag: Iran","tags":["IR","flag","flag: iran"],"emoji":"🇮🇷","order":4887,"group":9,"version":2},{"shortcodes":["flag_is","iceland"],"annotation":"flag: Iceland","tags":["IS","flag","flag: iceland"],"emoji":"🇮🇸","order":4888,"group":9,"version":2},{"shortcodes":["flag_it","italy"],"annotation":"flag: Italy","tags":["IT","flag","flag: italy"],"emoji":"🇮🇹","order":4889,"group":9,"version":0.6},{"shortcodes":["flag_je","jersey"],"annotation":"flag: Jersey","tags":["JE","flag","flag: jersey"],"emoji":"🇯🇪","order":4890,"group":9,"version":2},{"shortcodes":["flag_jm","jamaica"],"annotation":"flag: Jamaica","tags":["JM","flag","flag: jamaica"],"emoji":"🇯🇲","order":4891,"group":9,"version":2},{"shortcodes":["flag_jo","jordan"],"annotation":"flag: Jordan","tags":["JO","flag","flag: jordan"],"emoji":"🇯🇴","order":4892,"group":9,"version":2},{"shortcodes":["flag_jp","japan"],"annotation":"flag: Japan","tags":["JP","flag","flag: japan"],"emoji":"🇯🇵","order":4893,"group":9,"version":0.6},{"shortcodes":["flag_ke","kenya"],"annotation":"flag: Kenya","tags":["KE","flag","flag: kenya"],"emoji":"🇰🇪","order":4894,"group":9,"version":2},{"shortcodes":["flag_kg","kyrgyzstan"],"annotation":"flag: Kyrgyzstan","tags":["KG","flag","flag: kyrgyzstan"],"emoji":"🇰🇬","order":4895,"group":9,"version":2},{"shortcodes":["cambodia","flag_kh"],"annotation":"flag: Cambodia","tags":["KH","flag","flag: cambodia"],"emoji":"🇰🇭","order":4896,"group":9,"version":2},{"shortcodes":["flag_ki","kiribati"],"annotation":"flag: Kiribati","tags":["KI","flag","flag: kiribati"],"emoji":"🇰🇮","order":4897,"group":9,"version":2},{"shortcodes":["comoros","flag_km"],"annotation":"flag: Comoros","tags":["KM","flag","flag: comoros"],"emoji":"🇰🇲","order":4898,"group":9,"version":2},{"shortcodes":["flag_kn","st_kitts_nevis"],"annotation":"flag: St. Kitts & Nevis","tags":["KN","flag","flag: st. kitts & nevis"],"emoji":"🇰🇳","order":4899,"group":9,"version":2},{"shortcodes":["flag_kp","north_korea"],"annotation":"flag: North Korea","tags":["KP","flag","flag: north korea"],"emoji":"🇰🇵","order":4900,"group":9,"version":2},{"shortcodes":["flag_kr","south_korea"],"annotation":"flag: South Korea","tags":["KR","flag","flag: south korea"],"emoji":"🇰🇷","order":4901,"group":9,"version":0.6},{"shortcodes":["flag_kw","kuwait"],"annotation":"flag: Kuwait","tags":["KW","flag","flag: kuwait"],"emoji":"🇰🇼","order":4902,"group":9,"version":2},{"shortcodes":["cayman_islands","flag_ky"],"annotation":"flag: Cayman Islands","tags":["KY","flag","flag: cayman islands"],"emoji":"🇰🇾","order":4903,"group":9,"version":2},{"shortcodes":["flag_kz","kazakhstan"],"annotation":"flag: Kazakhstan","tags":["KZ","flag","flag: kazakhstan"],"emoji":"🇰🇿","order":4904,"group":9,"version":2},{"shortcodes":["flag_la","laos"],"annotation":"flag: Laos","tags":["LA","flag","flag: laos"],"emoji":"🇱🇦","order":4905,"group":9,"version":2},{"shortcodes":["flag_lb","lebanon"],"annotation":"flag: Lebanon","tags":["LB","flag","flag: lebanon"],"emoji":"🇱🇧","order":4906,"group":9,"version":2},{"shortcodes":["flag_lc","st_lucia"],"annotation":"flag: St. Lucia","tags":["LC","flag","flag: st. lucia"],"emoji":"🇱🇨","order":4907,"group":9,"version":2},{"shortcodes":["flag_li","liechtenstein"],"annotation":"flag: Liechtenstein","tags":["LI","flag","flag: liechtenstein"],"emoji":"🇱🇮","order":4908,"group":9,"version":2},{"shortcodes":["flag_lk","sri_lanka"],"annotation":"flag: Sri Lanka","tags":["LK","flag","flag: sri lanka"],"emoji":"🇱🇰","order":4909,"group":9,"version":2},{"shortcodes":["flag_lr","liberia"],"annotation":"flag: Liberia","tags":["LR","flag","flag: liberia"],"emoji":"🇱🇷","order":4910,"group":9,"version":2},{"shortcodes":["flag_ls","lesotho"],"annotation":"flag: Lesotho","tags":["LS","flag","flag: lesotho"],"emoji":"🇱🇸","order":4911,"group":9,"version":2},{"shortcodes":["flag_lt","lithuania"],"annotation":"flag: Lithuania","tags":["LT","flag","flag: lithuania"],"emoji":"🇱🇹","order":4912,"group":9,"version":2},{"shortcodes":["flag_lu","luxembourg"],"annotation":"flag: Luxembourg","tags":["LU","flag","flag: luxembourg"],"emoji":"🇱🇺","order":4913,"group":9,"version":2},{"shortcodes":["flag_lv","latvia"],"annotation":"flag: Latvia","tags":["LV","flag","flag: latvia"],"emoji":"🇱🇻","order":4914,"group":9,"version":2},{"shortcodes":["flag_ly","libya"],"annotation":"flag: Libya","tags":["LY","flag","flag: libya"],"emoji":"🇱🇾","order":4915,"group":9,"version":2},{"shortcodes":["flag_ma","morocco"],"annotation":"flag: Morocco","tags":["MA","flag","flag: morocco"],"emoji":"🇲🇦","order":4916,"group":9,"version":2},{"shortcodes":["flag_mc","monaco"],"annotation":"flag: Monaco","tags":["MC","flag","flag: monaco"],"emoji":"🇲🇨","order":4917,"group":9,"version":2},{"shortcodes":["flag_md","moldova"],"annotation":"flag: Moldova","tags":["MD","flag","flag: moldova"],"emoji":"🇲🇩","order":4918,"group":9,"version":2},{"shortcodes":["flag_me","montenegro"],"annotation":"flag: Montenegro","tags":["ME","flag","flag: montenegro"],"emoji":"🇲🇪","order":4919,"group":9,"version":2},{"shortcodes":["flag_mf","st_martin"],"annotation":"flag: St. Martin","tags":["MF","flag","flag: st. martin"],"emoji":"🇲🇫","order":4920,"group":9,"version":2},{"shortcodes":["flag_mg","madagascar"],"annotation":"flag: Madagascar","tags":["MG","flag","flag: madagascar"],"emoji":"🇲🇬","order":4921,"group":9,"version":2},{"shortcodes":["flag_mh","marshall_islands"],"annotation":"flag: Marshall Islands","tags":["MH","flag","flag: marshall islands"],"emoji":"🇲🇭","order":4922,"group":9,"version":2},{"shortcodes":["flag_mk","macedonia"],"annotation":"flag: North Macedonia","tags":["MK","flag","flag: north macedonia"],"emoji":"🇲🇰","order":4923,"group":9,"version":2},{"shortcodes":["flag_ml","mali"],"annotation":"flag: Mali","tags":["ML","flag","flag: mali"],"emoji":"🇲🇱","order":4924,"group":9,"version":2},{"shortcodes":["burma","flag_mm","myanmar"],"annotation":"flag: Myanmar (Burma)","tags":["MM","flag","flag: myanmar (burma)"],"emoji":"🇲🇲","order":4925,"group":9,"version":2},{"shortcodes":["flag_mn","mongolia"],"annotation":"flag: Mongolia","tags":["MN","flag","flag: mongolia"],"emoji":"🇲🇳","order":4926,"group":9,"version":2},{"shortcodes":["flag_mo","macao","macau"],"annotation":"flag: Macao SAR China","tags":["MO","flag","flag: macao sar china"],"emoji":"🇲🇴","order":4927,"group":9,"version":2},{"shortcodes":["flag_mp","northern_mariana_islands"],"annotation":"flag: Northern Mariana Islands","tags":["MP","flag","flag: northern mariana islands"],"emoji":"🇲🇵","order":4928,"group":9,"version":2},{"shortcodes":["flag_mq","martinique"],"annotation":"flag: Martinique","tags":["MQ","flag","flag: martinique"],"emoji":"🇲🇶","order":4929,"group":9,"version":2},{"shortcodes":["flag_mr","mauritania"],"annotation":"flag: Mauritania","tags":["MR","flag","flag: mauritania"],"emoji":"🇲🇷","order":4930,"group":9,"version":2},{"shortcodes":["flag_ms","montserrat"],"annotation":"flag: Montserrat","tags":["MS","flag","flag: montserrat"],"emoji":"🇲🇸","order":4931,"group":9,"version":2},{"shortcodes":["flag_mt","malta"],"annotation":"flag: Malta","tags":["MT","flag","flag: malta"],"emoji":"🇲🇹","order":4932,"group":9,"version":2},{"shortcodes":["flag_mu","mauritius"],"annotation":"flag: Mauritius","tags":["MU","flag","flag: mauritius"],"emoji":"🇲🇺","order":4933,"group":9,"version":2},{"shortcodes":["flag_mv","maldives"],"annotation":"flag: Maldives","tags":["MV","flag","flag: maldives"],"emoji":"🇲🇻","order":4934,"group":9,"version":2},{"shortcodes":["flag_mw","malawi"],"annotation":"flag: Malawi","tags":["MW","flag","flag: malawi"],"emoji":"🇲🇼","order":4935,"group":9,"version":2},{"shortcodes":["flag_mx","mexico"],"annotation":"flag: Mexico","tags":["MX","flag","flag: mexico"],"emoji":"🇲🇽","order":4936,"group":9,"version":2},{"shortcodes":["flag_my","malaysia"],"annotation":"flag: Malaysia","tags":["MY","flag","flag: malaysia"],"emoji":"🇲🇾","order":4937,"group":9,"version":2},{"shortcodes":["flag_mz","mozambique"],"annotation":"flag: Mozambique","tags":["MZ","flag","flag: mozambique"],"emoji":"🇲🇿","order":4938,"group":9,"version":2},{"shortcodes":["flag_na","namibia"],"annotation":"flag: Namibia","tags":["NA","flag","flag: namibia"],"emoji":"🇳🇦","order":4939,"group":9,"version":2},{"shortcodes":["flag_nc","new_caledonia"],"annotation":"flag: New Caledonia","tags":["NC","flag","flag: new caledonia"],"emoji":"🇳🇨","order":4940,"group":9,"version":2},{"shortcodes":["flag_ne","niger"],"annotation":"flag: Niger","tags":["NE","flag","flag: niger"],"emoji":"🇳🇪","order":4941,"group":9,"version":2},{"shortcodes":["flag_nf","norfolk_island"],"annotation":"flag: Norfolk Island","tags":["NF","flag","flag: norfolk island"],"emoji":"🇳🇫","order":4942,"group":9,"version":2},{"shortcodes":["flag_ng","nigeria"],"annotation":"flag: Nigeria","tags":["NG","flag","flag: nigeria"],"emoji":"🇳🇬","order":4943,"group":9,"version":2},{"shortcodes":["flag_ni","nicaragua"],"annotation":"flag: Nicaragua","tags":["NI","flag","flag: nicaragua"],"emoji":"🇳🇮","order":4944,"group":9,"version":2},{"shortcodes":["flag_nl","netherlands"],"annotation":"flag: Netherlands","tags":["NL","flag","flag: netherlands"],"emoji":"🇳🇱","order":4945,"group":9,"version":2},{"shortcodes":["flag_no","norway"],"annotation":"flag: Norway","tags":["NO","flag","flag: norway"],"emoji":"🇳🇴","order":4946,"group":9,"version":2},{"shortcodes":["flag_np","nepal"],"annotation":"flag: Nepal","tags":["NP","flag","flag: nepal"],"emoji":"🇳🇵","order":4947,"group":9,"version":2},{"shortcodes":["flag_nr","nauru"],"annotation":"flag: Nauru","tags":["NR","flag","flag: nauru"],"emoji":"🇳🇷","order":4948,"group":9,"version":2},{"shortcodes":["flag_nu","niue"],"annotation":"flag: Niue","tags":["NU","flag","flag: niue"],"emoji":"🇳🇺","order":4949,"group":9,"version":2},{"shortcodes":["flag_nz","new_zealand"],"annotation":"flag: New Zealand","tags":["NZ","flag","flag: new zealand"],"emoji":"🇳🇿","order":4950,"group":9,"version":2},{"shortcodes":["flag_om","oman"],"annotation":"flag: Oman","tags":["OM","flag","flag: oman"],"emoji":"🇴🇲","order":4951,"group":9,"version":2},{"shortcodes":["flag_pa","panama"],"annotation":"flag: Panama","tags":["PA","flag","flag: panama"],"emoji":"🇵🇦","order":4952,"group":9,"version":2},{"shortcodes":["flag_pe","peru"],"annotation":"flag: Peru","tags":["PE","flag","flag: peru"],"emoji":"🇵🇪","order":4953,"group":9,"version":2},{"shortcodes":["flag_pf","french_polynesia"],"annotation":"flag: French Polynesia","tags":["PF","flag","flag: french polynesia"],"emoji":"🇵🇫","order":4954,"group":9,"version":2},{"shortcodes":["flag_pg","papua_new_guinea"],"annotation":"flag: Papua New Guinea","tags":["PG","flag","flag: papua new guinea"],"emoji":"🇵🇬","order":4955,"group":9,"version":2},{"shortcodes":["flag_ph","philippines"],"annotation":"flag: Philippines","tags":["PH","flag","flag: philippines"],"emoji":"🇵🇭","order":4956,"group":9,"version":2},{"shortcodes":["flag_pk","pakistan"],"annotation":"flag: Pakistan","tags":["PK","flag","flag: pakistan"],"emoji":"🇵🇰","order":4957,"group":9,"version":2},{"shortcodes":["flag_pl","poland"],"annotation":"flag: Poland","tags":["PL","flag","flag: poland"],"emoji":"🇵🇱","order":4958,"group":9,"version":2},{"shortcodes":["flag_pm","st_pierre_miquelon"],"annotation":"flag: St. Pierre & Miquelon","tags":["PM","flag","flag: st. pierre & miquelon"],"emoji":"🇵🇲","order":4959,"group":9,"version":2},{"shortcodes":["flag_pn","pitcairn_islands"],"annotation":"flag: Pitcairn Islands","tags":["PN","flag","flag: pitcairn islands"],"emoji":"🇵🇳","order":4960,"group":9,"version":2},{"shortcodes":["flag_pr","puerto_rico"],"annotation":"flag: Puerto Rico","tags":["PR","flag","flag: puerto rico"],"emoji":"🇵🇷","order":4961,"group":9,"version":2},{"shortcodes":["flag_ps","palestinian_territories"],"annotation":"flag: Palestinian Territories","tags":["PS","flag","flag: palestinian territories"],"emoji":"🇵🇸","order":4962,"group":9,"version":2},{"shortcodes":["flag_pt","portugal"],"annotation":"flag: Portugal","tags":["PT","flag","flag: portugal"],"emoji":"🇵🇹","order":4963,"group":9,"version":2},{"shortcodes":["flag_pw","palau"],"annotation":"flag: Palau","tags":["PW","flag","flag: palau"],"emoji":"🇵🇼","order":4964,"group":9,"version":2},{"shortcodes":["flag_py","paraguay"],"annotation":"flag: Paraguay","tags":["PY","flag","flag: paraguay"],"emoji":"🇵🇾","order":4965,"group":9,"version":2},{"shortcodes":["flag_qa","qatar"],"annotation":"flag: Qatar","tags":["QA","flag","flag: qatar"],"emoji":"🇶🇦","order":4966,"group":9,"version":2},{"shortcodes":["flag_re","reunion"],"annotation":"flag: Réunion","tags":["RE","flag","flag: réunion"],"emoji":"🇷🇪","order":4967,"group":9,"version":2},{"shortcodes":["flag_ro","romania"],"annotation":"flag: Romania","tags":["RO","flag","flag: romania"],"emoji":"🇷🇴","order":4968,"group":9,"version":2},{"shortcodes":["flag_rs","serbia"],"annotation":"flag: Serbia","tags":["RS","flag","flag: serbia"],"emoji":"🇷🇸","order":4969,"group":9,"version":2},{"shortcodes":["flag_ru","russia"],"annotation":"flag: Russia","tags":["RU","flag","flag: russia"],"emoji":"🇷🇺","order":4970,"group":9,"version":0.6},{"shortcodes":["flag_rw","rwanda"],"annotation":"flag: Rwanda","tags":["RW","flag","flag: rwanda"],"emoji":"🇷🇼","order":4971,"group":9,"version":2},{"shortcodes":["flag_sa","saudi_arabia"],"annotation":"flag: Saudi Arabia","tags":["SA","flag","flag: saudi arabia"],"emoji":"🇸🇦","order":4972,"group":9,"version":2},{"shortcodes":["flag_sb","solomon_islands"],"annotation":"flag: Solomon Islands","tags":["SB","flag","flag: solomon islands"],"emoji":"🇸🇧","order":4973,"group":9,"version":2},{"shortcodes":["flag_sc","seychelles"],"annotation":"flag: Seychelles","tags":["SC","flag","flag: seychelles"],"emoji":"🇸🇨","order":4974,"group":9,"version":2},{"shortcodes":["flag_sd","sudan"],"annotation":"flag: Sudan","tags":["SD","flag","flag: sudan"],"emoji":"🇸🇩","order":4975,"group":9,"version":2},{"shortcodes":["flag_se","sweden"],"annotation":"flag: Sweden","tags":["SE","flag","flag: sweden"],"emoji":"🇸🇪","order":4976,"group":9,"version":2},{"shortcodes":["flag_sg","singapore"],"annotation":"flag: Singapore","tags":["SG","flag","flag: singapore"],"emoji":"🇸🇬","order":4977,"group":9,"version":2},{"shortcodes":["flag_sh","st_helena"],"annotation":"flag: St. Helena","tags":["SH","flag","flag: st. helena"],"emoji":"🇸🇭","order":4978,"group":9,"version":2},{"shortcodes":["flag_si","slovenia"],"annotation":"flag: Slovenia","tags":["SI","flag","flag: slovenia"],"emoji":"🇸🇮","order":4979,"group":9,"version":2},{"shortcodes":["flag_sj","svalbard_jan_mayen"],"annotation":"flag: Svalbard & Jan Mayen","tags":["SJ","flag","flag: svalbard & jan mayen"],"emoji":"🇸🇯","order":4980,"group":9,"version":2},{"shortcodes":["flag_sk","slovakia"],"annotation":"flag: Slovakia","tags":["SK","flag","flag: slovakia"],"emoji":"🇸🇰","order":4981,"group":9,"version":2},{"shortcodes":["flag_sl","sierra_leone"],"annotation":"flag: Sierra Leone","tags":["SL","flag","flag: sierra leone"],"emoji":"🇸🇱","order":4982,"group":9,"version":2},{"shortcodes":["flag_sm","san_marino"],"annotation":"flag: San Marino","tags":["SM","flag","flag: san marino"],"emoji":"🇸🇲","order":4983,"group":9,"version":2},{"shortcodes":["flag_sn","senegal"],"annotation":"flag: Senegal","tags":["SN","flag","flag: senegal"],"emoji":"🇸🇳","order":4984,"group":9,"version":2},{"shortcodes":["flag_so","somalia"],"annotation":"flag: Somalia","tags":["SO","flag","flag: somalia"],"emoji":"🇸🇴","order":4985,"group":9,"version":2},{"shortcodes":["flag_sr","suriname"],"annotation":"flag: Suriname","tags":["SR","flag","flag: suriname"],"emoji":"🇸🇷","order":4986,"group":9,"version":2},{"shortcodes":["flag_ss","south_sudan"],"annotation":"flag: South Sudan","tags":["SS","flag","flag: south sudan"],"emoji":"🇸🇸","order":4987,"group":9,"version":2},{"shortcodes":["flag_st","sao_tome_principe"],"annotation":"flag: São Tomé & Príncipe","tags":["ST","flag","flag: são tomé & príncipe"],"emoji":"🇸🇹","order":4988,"group":9,"version":2},{"shortcodes":["el_salvador","flag_sv"],"annotation":"flag: El Salvador","tags":["SV","flag","flag: el salvador"],"emoji":"🇸🇻","order":4989,"group":9,"version":2},{"shortcodes":["flag_sx","sint_maarten"],"annotation":"flag: Sint Maarten","tags":["SX","flag","flag: sint maarten"],"emoji":"🇸🇽","order":4990,"group":9,"version":2},{"shortcodes":["flag_sy","syria"],"annotation":"flag: Syria","tags":["SY","flag","flag: syria"],"emoji":"🇸🇾","order":4991,"group":9,"version":2},{"shortcodes":["eswatini","flag_sz","swaziland"],"annotation":"flag: Eswatini","tags":["SZ","flag","flag: eswatini"],"emoji":"🇸🇿","order":4992,"group":9,"version":2},{"shortcodes":["flag_ta","tristan_da_cunha"],"annotation":"flag: Tristan da Cunha","tags":["TA","flag","flag: tristan da cunha"],"emoji":"🇹🇦","order":4993,"group":9,"version":2},{"shortcodes":["flag_tc","turks_caicos_islands"],"annotation":"flag: Turks & Caicos Islands","tags":["TC","flag","flag: turks & caicos islands"],"emoji":"🇹🇨","order":4994,"group":9,"version":2},{"shortcodes":["chad","flag_td"],"annotation":"flag: Chad","tags":["TD","flag","flag: chad"],"emoji":"🇹🇩","order":4995,"group":9,"version":2},{"shortcodes":["flag_tf","french_southern_territories"],"annotation":"flag: French Southern Territories","tags":["TF","flag","flag: french southern territories"],"emoji":"🇹🇫","order":4996,"group":9,"version":2},{"shortcodes":["flag_tg","togo"],"annotation":"flag: Togo","tags":["TG","flag","flag: togo"],"emoji":"🇹🇬","order":4997,"group":9,"version":2},{"shortcodes":["flag_th","thailand"],"annotation":"flag: Thailand","tags":["TH","flag","flag: thailand"],"emoji":"🇹🇭","order":4998,"group":9,"version":2},{"shortcodes":["flag_tj","tajikistan"],"annotation":"flag: Tajikistan","tags":["TJ","flag","flag: tajikistan"],"emoji":"🇹🇯","order":4999,"group":9,"version":2},{"shortcodes":["flag_tk","tokelau"],"annotation":"flag: Tokelau","tags":["TK","flag","flag: tokelau"],"emoji":"🇹🇰","order":5000,"group":9,"version":2},{"shortcodes":["flag_tl","timor_leste"],"annotation":"flag: Timor-Leste","tags":["TL","flag","flag: timor-leste"],"emoji":"🇹🇱","order":5001,"group":9,"version":2},{"shortcodes":["flag_tm","turkmenistan"],"annotation":"flag: Turkmenistan","tags":["TM","flag","flag: turkmenistan"],"emoji":"🇹🇲","order":5002,"group":9,"version":2},{"shortcodes":["flag_tn","tunisia"],"annotation":"flag: Tunisia","tags":["TN","flag","flag: tunisia"],"emoji":"🇹🇳","order":5003,"group":9,"version":2},{"shortcodes":["flag_to","tonga"],"annotation":"flag: Tonga","tags":["TO","flag","flag: tonga"],"emoji":"🇹🇴","order":5004,"group":9,"version":2},{"shortcodes":["flag_tr","turkey_tr"],"annotation":"flag: Türkiye","tags":["TR","flag","flag: türkiye"],"emoji":"🇹🇷","order":5005,"group":9,"version":2},{"shortcodes":["flag_tt","trinidad_tobago"],"annotation":"flag: Trinidad & Tobago","tags":["TT","flag","flag: trinidad & tobago"],"emoji":"🇹🇹","order":5006,"group":9,"version":2},{"shortcodes":["flag_tv","tuvalu"],"annotation":"flag: Tuvalu","tags":["TV","flag","flag: tuvalu"],"emoji":"🇹🇻","order":5007,"group":9,"version":2},{"shortcodes":["flag_tw","taiwan"],"annotation":"flag: Taiwan","tags":["TW","flag","flag: taiwan"],"emoji":"🇹🇼","order":5008,"group":9,"version":2},{"shortcodes":["flag_tz","tanzania"],"annotation":"flag: Tanzania","tags":["TZ","flag","flag: tanzania"],"emoji":"🇹🇿","order":5009,"group":9,"version":2},{"shortcodes":["flag_ua","ukraine"],"annotation":"flag: Ukraine","tags":["UA","flag","flag: ukraine"],"emoji":"🇺🇦","order":5010,"group":9,"version":2},{"shortcodes":["flag_ug","uganda"],"annotation":"flag: Uganda","tags":["UG","flag","flag: uganda"],"emoji":"🇺🇬","order":5011,"group":9,"version":2},{"shortcodes":["flag_um","us_outlying_islands"],"annotation":"flag: U.S. Outlying Islands","tags":["UM","flag","flag: u.s. outlying islands"],"emoji":"🇺🇲","order":5012,"group":9,"version":2},{"shortcodes":["flag_un","un","united_nations"],"annotation":"flag: United Nations","tags":["UN","flag","flag: united nations"],"emoji":"🇺🇳","order":5013,"group":9,"version":4},{"shortcodes":["flag_us","united_states","usa"],"annotation":"flag: United States","tags":["US","flag","flag: united states"],"emoji":"🇺🇸","order":5014,"group":9,"version":0.6},{"shortcodes":["flag_uy","uruguay"],"annotation":"flag: Uruguay","tags":["UY","flag","flag: uruguay"],"emoji":"🇺🇾","order":5015,"group":9,"version":2},{"shortcodes":["flag_uz","uzbekistan"],"annotation":"flag: Uzbekistan","tags":["UZ","flag","flag: uzbekistan"],"emoji":"🇺🇿","order":5016,"group":9,"version":2},{"shortcodes":["flag_va","vatican_city"],"annotation":"flag: Vatican City","tags":["VA","flag","flag: vatican city"],"emoji":"🇻🇦","order":5017,"group":9,"version":2},{"shortcodes":["flag_vc","st_vincent_grenadines"],"annotation":"flag: St. Vincent & Grenadines","tags":["VC","flag","flag: st. vincent & grenadines"],"emoji":"🇻🇨","order":5018,"group":9,"version":2},{"shortcodes":["flag_ve","venezuela"],"annotation":"flag: Venezuela","tags":["VE","flag","flag: venezuela"],"emoji":"🇻🇪","order":5019,"group":9,"version":2},{"shortcodes":["british_virgin_islands","flag_vg"],"annotation":"flag: British Virgin Islands","tags":["VG","flag","flag: british virgin islands"],"emoji":"🇻🇬","order":5020,"group":9,"version":2},{"shortcodes":["flag_vi","us_virgin_islands"],"annotation":"flag: U.S. Virgin Islands","tags":["VI","flag","flag: u.s. virgin islands"],"emoji":"🇻🇮","order":5021,"group":9,"version":2},{"shortcodes":["flag_vn","vietnam"],"annotation":"flag: Vietnam","tags":["VN","flag","flag: vietnam"],"emoji":"🇻🇳","order":5022,"group":9,"version":2},{"shortcodes":["flag_vu","vanuatu"],"annotation":"flag: Vanuatu","tags":["VU","flag","flag: vanuatu"],"emoji":"🇻🇺","order":5023,"group":9,"version":2},{"shortcodes":["flag_wf","wallis_futuna"],"annotation":"flag: Wallis & Futuna","tags":["WF","flag","flag: wallis & futuna"],"emoji":"🇼🇫","order":5024,"group":9,"version":2},{"shortcodes":["flag_ws","samoa"],"annotation":"flag: Samoa","tags":["WS","flag","flag: samoa"],"emoji":"🇼🇸","order":5025,"group":9,"version":2},{"shortcodes":["flag_xk","kosovo"],"annotation":"flag: Kosovo","tags":["XK","flag","flag: kosovo"],"emoji":"🇽🇰","order":5026,"group":9,"version":2},{"shortcodes":["flag_ye","yemen"],"annotation":"flag: Yemen","tags":["YE","flag","flag: yemen"],"emoji":"🇾🇪","order":5027,"group":9,"version":2},{"shortcodes":["flag_yt","mayotte"],"annotation":"flag: Mayotte","tags":["YT","flag","flag: mayotte"],"emoji":"🇾🇹","order":5028,"group":9,"version":2},{"shortcodes":["flag_za","south_africa"],"annotation":"flag: South Africa","tags":["ZA","flag","flag: south africa"],"emoji":"🇿🇦","order":5029,"group":9,"version":2},{"shortcodes":["flag_zm","zambia"],"annotation":"flag: Zambia","tags":["ZM","flag","flag: zambia"],"emoji":"🇿🇲","order":5030,"group":9,"version":2},{"shortcodes":["flag_zw","zimbabwe"],"annotation":"flag: Zimbabwe","tags":["ZW","flag","flag: zimbabwe"],"emoji":"🇿🇼","order":5031,"group":9,"version":2},{"shortcodes":["england","flag_gbeng"],"annotation":"flag: England","tags":["flag","flag: england","gbeng"],"emoji":"🏴󠁧󠁢󠁥󠁮󠁧󠁿","order":5032,"group":9,"version":5},{"shortcodes":["flag_gbsct","scotland"],"annotation":"flag: Scotland","tags":["flag","flag: scotland","gbsct"],"emoji":"🏴󠁧󠁢󠁳󠁣󠁴󠁿","order":5033,"group":9,"version":5},{"shortcodes":["flag_gbwls","wales"],"annotation":"flag: Wales","tags":["flag","flag: wales","gbwls"],"emoji":"🏴󠁧󠁢󠁷󠁬󠁳󠁿","order":5034,"group":9,"version":5}] \ No newline at end of file diff --git a/frontend/src/components/input/Reactions.vue b/frontend/src/components/input/Reactions.vue new file mode 100644 index 000000000..16d4d5af9 --- /dev/null +++ b/frontend/src/components/input/Reactions.vue @@ -0,0 +1,193 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/input/editor/TipTap.vue b/frontend/src/components/input/editor/TipTap.vue index c39eaddcd..1cc3c165d 100644 --- a/frontend/src/components/input/editor/TipTap.vue +++ b/frontend/src/components/input/editor/TipTap.vue @@ -558,6 +558,10 @@ function handleImagePaste(event) { // See https://github.com/github/hotkey/discussions/85#discussioncomment-5214660 function setFocusToEditor(event) { + if(event.target.shadowRoot) { + return + } + const hotkeyString = eventToHotkeyString(event) if (!hotkeyString) return if (hotkeyString !== editShortcut || diff --git a/frontend/src/components/misc/Icon.ts b/frontend/src/components/misc/Icon.ts index d50e3953d..8b2d1a011 100644 --- a/frontend/src/components/misc/Icon.ts +++ b/frontend/src/components/misc/Icon.ts @@ -87,7 +87,7 @@ import { faStar, faSun, faTimesCircle, - faCircleQuestion, + faCircleQuestion, faFaceLaugh, } from '@fortawesome/free-regular-svg-icons' import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome' @@ -186,6 +186,7 @@ library.add(faXmarksLines) library.add(faFont) library.add(faRulerHorizontal) library.add(faUnderline) +library.add(faFaceLaugh) // overwriting the wrong types export default FontAwesomeIcon as unknown as FontAwesomeIconFixedTypes \ No newline at end of file diff --git a/frontend/src/components/tasks/partials/comments.vue b/frontend/src/components/tasks/partials/comments.vue index 188336f4f..d88ac8cbb 100644 --- a/frontend/src/components/tasks/partials/comments.vue +++ b/frontend/src/components/tasks/partials/comments.vue @@ -97,6 +97,12 @@ editComment() }" /> +
implements IReaction { + id: number = 0 + kind: 'tasks' | 'comments' = 'tasks' + value: string = '' + + constructor(data: Partial) { + super() + this.assignData(data) + } +} + \ No newline at end of file diff --git a/frontend/src/models/task.ts b/frontend/src/models/task.ts index 618fe35c1..2e2f1095a 100644 --- a/frontend/src/models/task.ts +++ b/frontend/src/models/task.ts @@ -86,6 +86,8 @@ export default class TaskModel extends AbstractModel implements ITask { position = 0 kanbanPosition = 0 + + reactions = {} createdBy: IUser = UserModel created: Date = null @@ -148,6 +150,12 @@ export default class TaskModel extends AbstractModel implements ITask { this.updated = new Date(this.updated) this.projectId = Number(this.projectId) + + // We can't convert emojis to camel case, hence we do this manually + this.reactions = {} + Object.keys(data.reactions || {}).forEach(reaction => { + this.reactions[reaction] = data.reactions[reaction].map(u => new UserModel(u)) + }) } getTextIdentifier() { diff --git a/frontend/src/models/taskComment.ts b/frontend/src/models/taskComment.ts index 4579140fc..5928ca5d6 100644 --- a/frontend/src/models/taskComment.ts +++ b/frontend/src/models/taskComment.ts @@ -10,6 +10,8 @@ export default class TaskCommentModel extends AbstractModel implem taskId: ITask['id'] = 0 comment = '' author: IUser = UserModel + + reactions = {} created: Date = null updated: Date = null @@ -21,5 +23,11 @@ export default class TaskCommentModel extends AbstractModel implem this.author = new UserModel(this.author) this.created = new Date(this.created) this.updated = new Date(this.updated) + + // We can't convert emojis to camel case, hence we do this manually + this.reactions = {} + Object.keys(data.reactions || {}).forEach(reaction => { + this.reactions[reaction] = data.reactions[reaction].map(u => new UserModel(u)) + }) } } diff --git a/frontend/src/services/abstractService.ts b/frontend/src/services/abstractService.ts index dc0b13f08..820347618 100644 --- a/frontend/src/services/abstractService.ts +++ b/frontend/src/services/abstractService.ts @@ -77,19 +77,25 @@ export default abstract class AbstractService): ReactionModel { + return new ReactionModel(data) + } + + modelGetAllFactory(data: Partial): Partial { + Object.keys(data).forEach(reaction => { + data[reaction] = data[reaction]?.map(u => new UserModel(u)) + }) + + return data + } + + async delete(model: IAbstract) { + const finalUrl = this.getReplacedRoute(this.paths.delete, model) + return super.post(finalUrl, model) + } +} diff --git a/frontend/src/services/task.ts b/frontend/src/services/task.ts index 48e1a8967..6071b3585 100644 --- a/frontend/src/services/task.ts +++ b/frontend/src/services/task.ts @@ -6,6 +6,7 @@ import LabelService from './label' import {colorFromHex} from '@/helpers/color/colorFromHex' import {SECONDS_A_DAY, SECONDS_A_HOUR, SECONDS_A_WEEK} from '@/constants/date' +import {objectToSnakeCase} from '@/helpers/case' const parseDate = date => { if (date) { @@ -38,8 +39,12 @@ export default class TaskService extends AbstractService { return this.processModel(model) } + autoTransformBeforePost(): boolean { + return false + } + processModel(updatedModel) { - const model = { ...updatedModel } + const model = {...updatedModel} model.title = model.title?.trim() @@ -108,7 +113,15 @@ export default class TaskService extends AbstractService { model.labels = model.labels.map(l => labelService.processModel(l)) } - return model as ITask + const transformed = objectToSnakeCase(model) + + // We can't convert emojis to skane case, hence we add them back again + transformed.reactions = {} + Object.keys(updatedModel.reactions || {}).forEach(reaction => { + transformed.reactions[reaction] = updatedModel.reactions[reaction].map(u => objectToSnakeCase(u)) + }) + + return transformed as ITask } } diff --git a/frontend/src/services/taskComment.ts b/frontend/src/services/taskComment.ts index 040a212fa..6eaa1aea4 100644 --- a/frontend/src/services/taskComment.ts +++ b/frontend/src/services/taskComment.ts @@ -1,6 +1,7 @@ import AbstractService from './abstractService' import TaskCommentModel from '@/models/taskComment' import type {ITaskComment} from '@/modelTypes/ITaskComment' +import {objectToSnakeCase} from '@/helpers/case' export default class TaskCommentService extends AbstractService { constructor() { @@ -16,4 +17,22 @@ export default class TaskCommentService extends AbstractService { modelFactory(data) { return new TaskCommentModel(data) } + + autoTransformBeforePost(): boolean { + return false + } + + beforeUpdate(model: ITaskComment) { + const transformed = objectToSnakeCase({...model}) + + // We can't convert emojis to skane case, hence we add them back again + transformed.reactions = {} + Object.keys(model.reactions || {}).forEach(reaction => { + transformed.reactions[reaction] = model.reactions[reaction].map(u => objectToSnakeCase(u)) + }) + + console.log() + + return transformed as ITaskComment + } } \ No newline at end of file diff --git a/frontend/src/stores/tasks.ts b/frontend/src/stores/tasks.ts index 332a4eba7..331625b89 100644 --- a/frontend/src/stores/tasks.ts +++ b/frontend/src/stores/tasks.ts @@ -152,6 +152,7 @@ export const useTaskStore = defineStore('task', () => { const taskService = new TaskService() try { const updatedTask = await taskService.update(task) + console.log({updated: updatedTask.reactions, old: task.reactions}) kanbanStore.setTaskInBucket(updatedTask) return updatedTask } finally { diff --git a/frontend/src/views/tasks/TaskDetailView.vue b/frontend/src/views/tasks/TaskDetailView.vue index da8c7c9a0..f3e274396 100644 --- a/frontend/src/views/tasks/TaskDetailView.vue +++ b/frontend/src/views/tasks/TaskDetailView.vue @@ -312,6 +312,14 @@ @update:modelValue="Object.assign(task, $event)" />
+ + +
. + +package migration + +import ( + "time" + + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" +) + +type reactions20240311173251 struct { + ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"reaction"` + UserID int64 `xorm:"bigint not null INDEX" json:"-"` + EntityID int64 `xorm:"bigint not null INDEX" json:"entity_id"` + EntityKind int `xorm:"bigint not null INDEX" json:"entity_kind"` + Value string `xorm:"varchar(20) not null INDEX" json:"value"` + Created time.Time `xorm:"created not null" json:"created"` +} + +func (reactions20240311173251) TableName() string { + return "reactions" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20240311173251", + Description: "Create reactions table", + Migrate: func(tx *xorm.Engine) error { + return tx.Sync2(reactions20240311173251{}) + }, + Rollback: func(tx *xorm.Engine) error { + return nil + }, + }) +} diff --git a/pkg/models/error.go b/pkg/models/error.go index 431d9e7d5..8eb151830 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -1060,6 +1060,33 @@ func (err ErrInvalidFilterExpression) HTTPError() web.HTTPError { } } +// ErrInvalidReactionEntityKind represents an error where the reaction kind is invalid +type ErrInvalidReactionEntityKind struct { + Kind string +} + +// IsErrInvalidReactionEntityKind checks if an error is ErrInvalidReactionEntityKind. +func IsErrInvalidReactionEntityKind(err error) bool { + _, ok := err.(ErrInvalidReactionEntityKind) + return ok +} + +func (err ErrInvalidReactionEntityKind) Error() string { + return fmt.Sprintf("Reaction kind %s is invalid", err.Kind) +} + +// ErrCodeInvalidReactionEntityKind holds the unique world-error code of this error +const ErrCodeInvalidReactionEntityKind = 4025 + +// HTTPError holds the http error description +func (err ErrInvalidReactionEntityKind) HTTPError() web.HTTPError { + return web.HTTPError{ + HTTPCode: http.StatusBadRequest, + Code: ErrCodeInvalidReactionEntityKind, + Message: fmt.Sprintf("The reaction kind '%s' is invalid.", err.Kind), + } +} + // ============ // Team errors // ============ diff --git a/pkg/models/models.go b/pkg/models/models.go index 7c35f4ed8..f8404978c 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -61,6 +61,7 @@ func GetTables() []interface{} { &APIToken{}, &TypesenseSync{}, &Webhook{}, + &Reaction{}, } } diff --git a/pkg/models/project.go b/pkg/models/project.go index 7958cbef5..7be2d933c 100644 --- a/pkg/models/project.go +++ b/pkg/models/project.go @@ -23,7 +23,6 @@ import ( "strings" "time" - "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/events" "code.vikunja.io/api/pkg/files" @@ -370,10 +369,7 @@ type projectOptions struct { } func getUserProjectsStatement(parentProjectIDs []int64, userID int64, search string, getArchived bool) *builder.Builder { - dialect := config.DatabaseType.GetString() - if dialect == "sqlite" { - dialect = builder.SQLITE - } + dialect := db.GetDialect() // Adding a 1=1 condition by default here because xorm always needs a condition and cannot handle nil conditions var getArchivedCond builder.Cond = builder.Eq{"1": 1} diff --git a/pkg/models/reaction.go b/pkg/models/reaction.go new file mode 100644 index 000000000..44fe73d62 --- /dev/null +++ b/pkg/models/reaction.go @@ -0,0 +1,191 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package models + +import ( + "time" + + "code.vikunja.io/web" + "xorm.io/builder" + "xorm.io/xorm" + + "code.vikunja.io/api/pkg/user" +) + +type ReactionKind int + +const ( + ReactionKindTask = iota + ReactionKindComment +) + +type Reaction struct { + // The unique numeric id of this reaction + ID int64 `xorm:"autoincr not null unique pk" json:"-" param:"reaction"` + + // The user who reacted + User *user.User `xorm:"-" json:"user" valid:"-"` + UserID int64 `xorm:"bigint not null INDEX" json:"-"` + + // The id of the entity you're reacting to + EntityID int64 `xorm:"bigint not null INDEX" json:"-" param:"entityid"` + // The entity kind which you're reacting to. Can be 0 for task, 1 for comment. + EntityKind ReactionKind `xorm:"bigint not null INDEX" json:"-"` + EntityKindString string `xorm:"-" json:"-" param:"entitykind"` + + // The actual reaction. This can be any valid utf character or text, up to a length of 20. + Value string `xorm:"varchar(20) not null INDEX" json:"value" valid:"required"` + + // A timestamp when this reaction was created. You cannot change this value. + Created time.Time `xorm:"created not null" json:"created"` + + web.CRUDable `xorm:"-" json:"-"` + web.Rights `xorm:"-" json:"-"` +} + +func (*Reaction) TableName() string { + return "reactions" +} + +type ReactionMap map[string][]*user.User + +// ReadAll gets all reactions for an entity +// @Summary Get all reactions for an entity +// @Description Returns all reactions for an entity +// @tags task +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Param id path int true "Entity ID" +// @Param kind path int true "The kind of the entity. Can be either `tasks` or `comments` for task comments" +// @Success 200 {array} models.ReactionMap "The reactions" +// @Failure 403 {object} web.HTTPError "The user does not have access to the entity" +// @Failure 500 {object} models.Message "Internal error" +// @Router /{kind}/{id}/reactions [get] +func (r *Reaction) ReadAll(s *xorm.Session, a web.Auth, _ string, _ int, _ int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) { + + can, _, err := r.CanRead(s, a) + if err != nil { + return nil, 0, 0, err + } + if !can { + return nil, 0, 0, ErrGenericForbidden{} + } + + reactions, err := getReactionsForEntityIDs(s, r.EntityKind, []int64{r.EntityID}) + if err != nil { + return + } + + return reactions[r.EntityID], len(reactions[r.EntityID]), int64(len(reactions[r.EntityID])), nil +} + +func getReactionsForEntityIDs(s *xorm.Session, entityKind ReactionKind, entityIDs []int64) (reactionsWithTasks map[int64]ReactionMap, err error) { + + where := builder.And( + builder.Eq{"entity_kind": entityKind}, + builder.In("entity_id", entityIDs), + ) + + reactions := []*Reaction{} + err = s.Where(where).Find(&reactions) + if err != nil { + return + } + + if len(reactions) == 0 { + return + } + + cond := builder. + Select("user_id"). + From("reactions"). + Where(where) + + users, err := user.GetUsersByCond(s, builder.In("id", cond)) + if err != nil { + return + } + + reactionsWithTasks = make(map[int64]ReactionMap) + for _, reaction := range reactions { + if _, taskExists := reactionsWithTasks[reaction.EntityID]; !taskExists { + reactionsWithTasks[reaction.EntityID] = make(ReactionMap) + } + + if _, has := reactionsWithTasks[reaction.EntityID][reaction.Value]; !has { + reactionsWithTasks[reaction.EntityID][reaction.Value] = []*user.User{} + } + + reactionsWithTasks[reaction.EntityID][reaction.Value] = append(reactionsWithTasks[reaction.EntityID][reaction.Value], users[reaction.UserID]) + } + + return +} + +// Delete removes the user's own reaction +// @Summary Removes the user's reaction +// @Description Removes the reaction of that user on that entity. +// @tags task +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Param id path int true "Entity ID" +// @Param kind path int true "The kind of the entity. Can be either `tasks` or `comments` for task comments" +// @Param project body models.Reaction true "The reaction you want to add to the entity." +// @Success 200 {object} models.Message "The reaction was successfully removed." +// @Failure 403 {object} web.HTTPError "The user does not have access to the entity" +// @Failure 500 {object} models.Message "Internal error" +// @Router /{kind}/{id}/reactions/delete [post] +func (r *Reaction) Delete(s *xorm.Session, a web.Auth) (err error) { + r.UserID = a.GetID() + + _, err = s.Where("user_id = ? AND entity_id = ? AND entity_kind = ? AND value = ?", r.UserID, r.EntityID, r.EntityKind, r.Value). + Delete(&Reaction{}) + return +} + +// Create adds a new reaction to an entity +// @Summary Add a reaction to an entity +// @Description Add a reaction to an entity. Will do nothing if the reaction already exists. +// @tags task +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Param id path int true "Entity ID" +// @Param kind path int true "The kind of the entity. Can be either `tasks` or `comments` for task comments" +// @Param project body models.Reaction true "The reaction you want to add to the entity." +// @Success 200 {object} models.Reaction "The created reaction" +// @Failure 403 {object} web.HTTPError "The user does not have access to the entity" +// @Failure 500 {object} models.Message "Internal error" +// @Router /{kind}/{id}/reactions [put] +func (r *Reaction) Create(s *xorm.Session, a web.Auth) (err error) { + r.UserID = a.GetID() + + exists, err := s.Where("user_id = ? AND entity_id = ? AND entity_kind = ? AND value = ?", r.UserID, r.EntityID, r.EntityKind, r.Value). + Exist(&Reaction{}) + if err != nil { + return err + } + + if exists { + return + } + + _, err = s.Insert(r) + return +} diff --git a/pkg/models/reaction_rights.go b/pkg/models/reaction_rights.go new file mode 100644 index 000000000..75ca684c1 --- /dev/null +++ b/pkg/models/reaction_rights.go @@ -0,0 +1,81 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package models + +import ( + "code.vikunja.io/web" + "xorm.io/xorm" +) + +func (r *Reaction) setEntityKindFromString() (err error) { + switch r.EntityKindString { + case "tasks": + r.EntityKind = ReactionKindTask + return + case "comments": + r.EntityKind = ReactionKindComment + return + } + + return ErrInvalidReactionEntityKind{ + Kind: r.EntityKindString, + } +} + +func (r *Reaction) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) { + t, err := r.getTask(s) + if err != nil { + return false, 0, err + } + return t.CanRead(s, a) +} + +func (r *Reaction) CanDelete(s *xorm.Session, a web.Auth) (bool, error) { + t, err := r.getTask(s) + if err != nil { + return false, err + } + return t.CanUpdate(s, a) +} + +func (r *Reaction) CanCreate(s *xorm.Session, a web.Auth) (bool, error) { + t, err := r.getTask(s) + if err != nil { + return false, err + } + return t.CanUpdate(s, a) +} + +func (r *Reaction) getTask(s *xorm.Session) (t *Task, err error) { + err = r.setEntityKindFromString() + if err != nil { + return + } + + t = &Task{ID: r.EntityID} + + if r.EntityKind == ReactionKindComment { + tc := &TaskComment{ID: r.EntityID} + err = getTaskCommentSimple(s, tc) + if err != nil { + return + } + t.ID = tc.TaskID + } + + return +} diff --git a/pkg/models/reaction_test.go b/pkg/models/reaction_test.go new file mode 100644 index 000000000..d788e450a --- /dev/null +++ b/pkg/models/reaction_test.go @@ -0,0 +1,217 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package models + +import ( + "testing" + + "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/user" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReaction_ReadAll(t *testing.T) { + t.Run("normal", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + u := &user.User{ID: 1} + + r := &Reaction{ + EntityID: 1, + EntityKindString: "tasks", + } + + reactions, _, _, err := r.ReadAll(s, u, "", 0, 0) + require.NoError(t, err) + assert.IsType(t, ReactionMap{}, reactions) + + reactionMap := reactions.(ReactionMap) + assert.Len(t, reactionMap["👋"], 1) + assert.Equal(t, int64(1), reactionMap["👋"][0].ID) + }) + t.Run("invalid entity", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + u := &user.User{ID: 1} + r := &Reaction{ + EntityID: 1, + EntityKindString: "loremipsum", + } + + _, _, _, err := r.ReadAll(s, u, "", 0, 0) + require.Error(t, err) + assert.ErrorIs(t, err, ErrInvalidReactionEntityKind{Kind: "loremipsum"}) + }) + t.Run("no access to task", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + u := &user.User{ID: 1} + + r := &Reaction{ + EntityID: 34, + EntityKindString: "tasks", + } + + _, _, _, err := r.ReadAll(s, u, "", 0, 0) + require.Error(t, err) + }) + t.Run("nonexistant task", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + u := &user.User{ID: 1} + r := &Reaction{ + EntityID: 9999999, + EntityKindString: "tasks", + } + + _, _, _, err := r.ReadAll(s, u, "", 0, 0) + require.Error(t, err) + assert.ErrorIs(t, err, ErrTaskDoesNotExist{ID: r.EntityID}) + }) + t.Run("no access to comment", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + u := &user.User{ID: 1} + + r := &Reaction{ + EntityID: 18, + EntityKindString: "comments", + } + + _, _, _, err := r.ReadAll(s, u, "", 0, 0) + require.Error(t, err) + }) + t.Run("nonexistant comment", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + u := &user.User{ID: 1} + r := &Reaction{ + EntityID: 9999999, + EntityKindString: "comments", + } + + _, _, _, err := r.ReadAll(s, u, "", 0, 0) + require.Error(t, err) + assert.ErrorIs(t, err, ErrTaskCommentDoesNotExist{ID: r.EntityID}) + }) +} + +func TestReaction_Create(t *testing.T) { + t.Run("normal", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + u := &user.User{ID: 1} + r := &Reaction{ + EntityID: 1, + EntityKindString: "tasks", + Value: "🦙", + } + + can, err := r.CanCreate(s, u) + require.NoError(t, err) + assert.True(t, can) + + err = r.Create(s, u) + require.NoError(t, err) + + err = s.Commit() + require.NoError(t, err) + + db.AssertExists(t, "reactions", map[string]interface{}{ + "entity_id": r.EntityID, + "entity_kind": ReactionKindTask, + "user_id": u.ID, + "value": r.Value, + }, false) + }) + t.Run("no permission to access task", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + u := &user.User{ID: 1} + r := &Reaction{ + EntityID: 34, + EntityKindString: "tasks", + Value: "🦙", + } + + can, err := r.CanCreate(s, u) + require.NoError(t, err) + assert.False(t, can) + }) + t.Run("no permission to access comment", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + u := &user.User{ID: 1} + r := &Reaction{ + EntityID: 18, + EntityKindString: "comments", + Value: "🦙", + } + + can, err := r.CanCreate(s, u) + require.NoError(t, err) + assert.False(t, can) + }) +} + +func TestReaction_Delete(t *testing.T) { + t.Run("normal", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + u := &user.User{ID: 1} + + r := &Reaction{ + EntityID: 1, + EntityKindString: "tasks", + Value: "👋", + } + + can, err := r.CanDelete(s, u) + require.NoError(t, err) + assert.True(t, can) + + err = r.Delete(s, u) + require.NoError(t, err) + + db.AssertMissing(t, "reactions", map[string]interface{}{ + "entity_id": r.EntityID, + "entity_kind": ReactionKindTask, + "value": "👋", + }) + }) +} diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index 0745b586a..63a8ab766 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -98,6 +98,9 @@ func TestTaskCollection_ReadAll(t *testing.T) { BucketID: 1, IsFavorite: true, Position: 2, + Reactions: ReactionMap{ + "👋": []*user.User{user1}, + }, Labels: []*Label{ label4, }, diff --git a/pkg/models/task_comments.go b/pkg/models/task_comments.go index 6c219a86a..ded53acf4 100644 --- a/pkg/models/task_comments.go +++ b/pkg/models/task_comments.go @@ -37,6 +37,8 @@ type TaskComment struct { Author *user.User `xorm:"-" json:"author"` TaskID int64 `xorm:"not null" json:"-" param:"task"` + Reactions ReactionMap `xorm:"-" json:"reactions"` + Created time.Time `xorm:"created" json:"created"` Updated time.Time `xorm:"updated" json:"updated"` @@ -167,7 +169,7 @@ func (tc *TaskComment) Update(s *xorm.Session, _ web.Auth) error { func getTaskCommentSimple(s *xorm.Session, tc *TaskComment) error { exists, err := s. - Where("id = ? and task_id = ?", tc.ID, tc.TaskID). + Where("id = ?", tc.ID). NoAutoCondition(). Get(tc) if err != nil { @@ -263,8 +265,10 @@ func (tc *TaskComment) ReadAll(s *xorm.Session, auth web.Auth, search string, pa } var authorIDs []int64 + var commentIDs []int64 for _, comment := range comments { authorIDs = append(authorIDs, comment.AuthorID) + commentIDs = append(commentIDs, comment.ID) } authors, err := getUsersOrLinkSharesFromIDs(s, authorIDs) @@ -272,8 +276,17 @@ func (tc *TaskComment) ReadAll(s *xorm.Session, auth web.Auth, search string, pa return } + reactions, err := getReactionsForEntityIDs(s, ReactionKindComment, commentIDs) + if err != nil { + return + } + for _, comment := range comments { comment.Author = authors[comment.AuthorID] + r, has := reactions[comment.ID] + if has { + comment.Reactions = r + } } numberOfTotalItems, err = s. diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 2f331d482..4a3b28da7 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -125,6 +125,9 @@ type Task struct { // The position of tasks in the kanban board. See the docs for the `position` property on how to use this. KanbanPosition float64 `xorm:"double null" json:"kanban_position"` + // Reactions on that task. + Reactions ReactionMap `xorm:"-" json:"reactions"` + // The user who initially created the task. CreatedBy *user.User `xorm:"-" json:"created_by" valid:"-"` CreatedByID int64 `xorm:"bigint not null" json:"-"` // ID of the user who put that task on the project @@ -584,6 +587,11 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth) (e return err } + reactions, err := getReactionsForEntityIDs(s, ReactionKindTask, taskIDs) + if err != nil { + return + } + // Add all objects to their tasks for _, task := range taskMap { @@ -600,6 +608,11 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth) (e task.setIdentifier(projects[task.ProjectID]) task.IsFavorite = taskFavorites[task.ID] + + r, has := reactions[task.ID] + if has { + task.Reactions = r + } } // Get all related tasks diff --git a/pkg/models/unit_tests.go b/pkg/models/unit_tests.go index 6dd66990b..b8f0a99de 100644 --- a/pkg/models/unit_tests.go +++ b/pkg/models/unit_tests.go @@ -65,6 +65,7 @@ func SetupTests() { "subscriptions", "favorites", "api_tokens", + "reactions", ) if err != nil { log.Fatal(err) diff --git a/pkg/modules/migration/trello/trello.go b/pkg/modules/migration/trello/trello.go index 380ec539c..d19fd48b7 100644 --- a/pkg/modules/migration/trello/trello.go +++ b/pkg/modules/migration/trello/trello.go @@ -25,6 +25,7 @@ import ( "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/modules/migration" "code.vikunja.io/api/pkg/user" + "github.com/adlio/trello" "github.com/yuin/goldmark" ) diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index 42934d2bf..d59970059 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -589,6 +589,15 @@ func registerAPIRoutes(a *echo.Group) { a.POST("/projects/:project/webhooks/:webhook", webhookProvider.UpdateWeb) a.GET("/webhooks/events", apiv1.GetAvailableWebhookEvents) } + + reactionProvider := &handler.WebHandler{ + EmptyStruct: func() handler.CObject { + return &models.Reaction{} + }, + } + a.GET("/:entitykind/:entityid/reactions", reactionProvider.ReadAllWeb) + a.POST("/:entitykind/:entityid/reactions/delete", reactionProvider.DeleteWeb) + a.PUT("/:entitykind/:entityid/reactions", reactionProvider.CreateWeb) } func registerMigrations(m *echo.Group) { diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 0fa109f4d..9f805a3e9 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -1,4 +1,5 @@ -// Package swagger Code generated by swaggo/swag. DO NOT EDIT +// Code generated by swaggo/swag. DO NOT EDIT. + package swagger import "github.com/swaggo/swag" @@ -7042,6 +7043,190 @@ const docTemplate = `{ } } }, + "/{kind}/{id}/reactions": { + "get": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Returns all reactions for an entity", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "task" + ], + "summary": "Get all reactions for an entity", + "parameters": [ + { + "type": "integer", + "description": "Entity ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "The kind of the entity. Can be either ` + "`" + `tasks` + "`" + ` or ` + "`" + `comments` + "`" + ` for task comments", + "name": "kind", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "The reactions", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ReactionMap" + } + } + }, + "403": { + "description": "The user does not have access to the entity", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + }, + "put": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "task" + ], + "summary": "Add a reaction to an entity", + "parameters": [ + { + "type": "integer", + "description": "Entity ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "The kind of the entity. Can be either ` + "`" + `tasks` + "`" + ` or ` + "`" + `comments` + "`" + ` for task comments", + "name": "kind", + "in": "path", + "required": true + }, + { + "description": "The reaction you want to add to the entity.", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Reaction" + } + } + ], + "responses": { + "200": { + "description": "The created reaction", + "schema": { + "$ref": "#/definitions/models.Reaction" + } + }, + "403": { + "description": "The user does not have access to the entity", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + }, + "delete": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Removes the reaction of that user on that entity.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "task" + ], + "summary": "Removes the user's reaction", + "parameters": [ + { + "type": "integer", + "description": "Entity ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "The kind of the entity. Can be either ` + "`" + `tasks` + "`" + ` or ` + "`" + `comments` + "`" + ` for task comments", + "name": "kind", + "in": "path", + "required": true + }, + { + "description": "The reaction you want to add to the entity.", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Reaction" + } + } + ], + "responses": { + "200": { + "description": "The reaction was successfully removed.", + "schema": { + "$ref": "#/definitions/models.Message" + } + }, + "403": { + "description": "The user does not have access to the entity", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + } + }, "/{username}/avatar": { "get": { "description": "Returns the user avatar as image.", @@ -7754,6 +7939,36 @@ const docTemplate = `{ } } }, + "models.Reaction": { + "type": "object", + "properties": { + "created": { + "description": "A timestamp when this reaction was created. You cannot change this value.", + "type": "string" + }, + "user": { + "description": "The user who reacted", + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] + }, + "value": { + "description": "The actual reaction. This can be any valid utf character or text, up to a length of 20.", + "type": "string" + } + } + }, + "models.ReactionMap": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/user.User" + } + } + }, "models.RelatedTaskMap": { "type": "object", "additionalProperties": { @@ -8975,8 +9190,6 @@ var SwaggerInfo = &swag.Spec{ Description: "# Pagination\nEvery endpoint capable of pagination will return two headers:\n* `x-pagination-total-pages`: The total number of available pages for this request\n* `x-pagination-result-count`: The number of items returned for this request.\n# Rights\nAll endpoints which return a single item (project, task, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`.\nThis can be used to show or hide ui elements based on the rights the user has.\n# Errors\nAll errors have an error code and a human-readable error message in addition to the http status code. You should always check for the status code in the response, not only the http status code.\nDue to limitations in the swagger library we're using for this document, only one error per http status code is documented here. Make sure to check the [error docs](https://vikunja.io/docs/errors/) in Vikunja's documentation for a full list of available error codes.\n# Authorization\n**JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer `-header to authenticate successfully.\n\n**API Token:** You can create scoped API tokens for your user and use the token to make authenticated requests in the context of that user. The token must be provided via an `Authorization: Bearer ` header, similar to jwt auth. See the documentation for the `api` group to manage token creation and revocation.\n\n**BasicAuth:** Only used when requesting tasks via CalDAV.\n", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, - LeftDelim: "{{", - RightDelim: "}}", } func init() { diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 0bc6e5b2e..b8624844f 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -7034,6 +7034,190 @@ } } }, + "/{kind}/{id}/reactions": { + "get": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Returns all reactions for an entity", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "task" + ], + "summary": "Get all reactions for an entity", + "parameters": [ + { + "type": "integer", + "description": "Entity ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "The kind of the entity. Can be either `tasks` or `comments` for task comments", + "name": "kind", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "The reactions", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ReactionMap" + } + } + }, + "403": { + "description": "The user does not have access to the entity", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + }, + "put": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "task" + ], + "summary": "Add a reaction to an entity", + "parameters": [ + { + "type": "integer", + "description": "Entity ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "The kind of the entity. Can be either `tasks` or `comments` for task comments", + "name": "kind", + "in": "path", + "required": true + }, + { + "description": "The reaction you want to add to the entity.", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Reaction" + } + } + ], + "responses": { + "200": { + "description": "The created reaction", + "schema": { + "$ref": "#/definitions/models.Reaction" + } + }, + "403": { + "description": "The user does not have access to the entity", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + }, + "delete": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Removes the reaction of that user on that entity.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "task" + ], + "summary": "Removes the user's reaction", + "parameters": [ + { + "type": "integer", + "description": "Entity ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "The kind of the entity. Can be either `tasks` or `comments` for task comments", + "name": "kind", + "in": "path", + "required": true + }, + { + "description": "The reaction you want to add to the entity.", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Reaction" + } + } + ], + "responses": { + "200": { + "description": "The reaction was successfully removed.", + "schema": { + "$ref": "#/definitions/models.Message" + } + }, + "403": { + "description": "The user does not have access to the entity", + "schema": { + "$ref": "#/definitions/web.HTTPError" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + } + }, "/{username}/avatar": { "get": { "description": "Returns the user avatar as image.", @@ -7746,6 +7930,36 @@ } } }, + "models.Reaction": { + "type": "object", + "properties": { + "created": { + "description": "A timestamp when this reaction was created. You cannot change this value.", + "type": "string" + }, + "user": { + "description": "The user who reacted", + "allOf": [ + { + "$ref": "#/definitions/user.User" + } + ] + }, + "value": { + "description": "The actual reaction. This can be any valid utf character or text, up to a length of 20.", + "type": "string" + } + } + }, + "models.ReactionMap": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/user.User" + } + } + }, "models.RelatedTaskMap": { "type": "object", "additionalProperties": { diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 5f6722b42..1accd27a6 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -509,6 +509,27 @@ definitions: description: The username. type: string type: object + models.Reaction: + properties: + created: + description: A timestamp when this reaction was created. You cannot change + this value. + type: string + user: + allOf: + - $ref: '#/definitions/user.User' + description: The user who reacted + value: + description: The actual reaction. This can be any valid utf character or text, + up to a length of 20. + type: string + type: object + models.ReactionMap: + additionalProperties: + items: + $ref: '#/definitions/user.User' + type: array + type: object models.RelatedTaskMap: additionalProperties: items: @@ -1439,6 +1460,128 @@ info: url: https://code.vikunja.io/api/src/branch/main/LICENSE title: Vikunja API paths: + /{kind}/{id}/reactions: + delete: + consumes: + - application/json + description: Removes the reaction of that user on that entity. + parameters: + - description: Entity ID + in: path + name: id + required: true + type: integer + - description: The kind of the entity. Can be either `tasks` or `comments` for + task comments + in: path + name: kind + required: true + type: integer + - description: The reaction you want to add to the entity. + in: body + name: project + required: true + schema: + $ref: '#/definitions/models.Reaction' + produces: + - application/json + responses: + "200": + description: The reaction was successfully removed. + schema: + $ref: '#/definitions/models.Message' + "403": + description: The user does not have access to the entity + schema: + $ref: '#/definitions/web.HTTPError' + "500": + description: Internal error + schema: + $ref: '#/definitions/models.Message' + security: + - JWTKeyAuth: [] + summary: Removes the user's reaction + tags: + - task + get: + consumes: + - application/json + description: Returns all reactions for an entity + parameters: + - description: Entity ID + in: path + name: id + required: true + type: integer + - description: The kind of the entity. Can be either `tasks` or `comments` for + task comments + in: path + name: kind + required: true + type: integer + produces: + - application/json + responses: + "200": + description: The reactions + schema: + items: + $ref: '#/definitions/models.ReactionMap' + type: array + "403": + description: The user does not have access to the entity + schema: + $ref: '#/definitions/web.HTTPError' + "500": + description: Internal error + schema: + $ref: '#/definitions/models.Message' + security: + - JWTKeyAuth: [] + summary: Get all reactions for an entity + tags: + - task + put: + consumes: + - application/json + parameters: + - description: Entity ID + in: path + name: id + required: true + type: integer + - description: The kind of the entity. Can be either `tasks` or `comments` for + task comments + in: path + name: kind + required: true + type: integer + - description: The reaction you want to add to the entity. + in: body + name: project + required: true + schema: + $ref: '#/definitions/models.Reaction' + produces: + - application/json + responses: + "200": + description: The created reaction + schema: + $ref: '#/definitions/models.Reaction' + "403": + description: The user does not have access to the entity + schema: + $ref: '#/definitions/web.HTTPError' + "500": + description: Internal error + schema: + $ref: '#/definitions/models.Message' + security: + - JWTKeyAuth: [] + summary: Add a reaction to an entity + tags: + - task /{username}/avatar: get: description: Returns the user avatar as image. diff --git a/pkg/user/user.go b/pkg/user/user.go index 43dc339c5..6feb6ad23 100644 --- a/pkg/user/user.go +++ b/pkg/user/user.go @@ -34,6 +34,7 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/labstack/echo/v4" "golang.org/x/crypto/bcrypt" + "xorm.io/builder" "xorm.io/xorm" ) @@ -259,13 +260,17 @@ func GetUserWithEmail(s *xorm.Session, user *User) (userOut *User, err error) { // GetUsersByIDs returns a map of users from a slice of user ids func GetUsersByIDs(s *xorm.Session, userIDs []int64) (users map[int64]*User, err error) { - users = make(map[int64]*User) - if len(userIDs) == 0 { return users, nil } - err = s.In("id", userIDs).Find(&users) + return GetUsersByCond(s, builder.In("id", userIDs)) +} + +func GetUsersByCond(s *xorm.Session, cond builder.Cond) (users map[int64]*User, err error) { + users = make(map[int64]*User) + + err = s.Where(cond).Find(&users) if err != nil { return } From 792bf88dcf973548069026d3eca6b50426a1f6f9 Mon Sep 17 00:00:00 2001 From: "Frederick [Bot]" Date: Tue, 12 Mar 2024 19:47:16 +0000 Subject: [PATCH 26/66] [skip ci] Updated swagger docs --- pkg/swagger/docs.go | 31 +++++++++++-- pkg/swagger/swagger.json | 26 ++++++++++- pkg/swagger/swagger.yaml | 97 +++++++++++++++++++++++----------------- 3 files changed, 106 insertions(+), 48 deletions(-) diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 9f805a3e9..a27794f94 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -1,5 +1,4 @@ -// Code generated by swaggo/swag. DO NOT EDIT. - +// Package swagger Code generated by swaggo/swag. DO NOT EDIT package swagger import "github.com/swaggo/swag" @@ -7107,6 +7106,7 @@ const docTemplate = `{ "JWTKeyAuth": [] } ], + "description": "Add a reaction to an entity. Will do nothing if the reaction already exists.", "consumes": [ "application/json" ], @@ -7162,8 +7162,10 @@ const docTemplate = `{ } } } - }, - "delete": { + } + }, + "/{kind}/{id}/reactions/delete": { + "post": { "security": [ { "JWTKeyAuth": [] @@ -7591,6 +7593,14 @@ const docTemplate = `{ "description": "The project this task belongs to.", "type": "integer" }, + "reactions": { + "description": "Reactions on that task.", + "allOf": [ + { + "$ref": "#/definitions/models.ReactionMap" + } + ] + }, "related_tasks": { "description": "All related tasks, grouped by their relation kind", "allOf": [ @@ -8239,6 +8249,14 @@ const docTemplate = `{ "description": "The project this task belongs to.", "type": "integer" }, + "reactions": { + "description": "Reactions on that task.", + "allOf": [ + { + "$ref": "#/definitions/models.ReactionMap" + } + ] + }, "related_tasks": { "description": "All related tasks, grouped by their relation kind", "allOf": [ @@ -8362,6 +8380,9 @@ const docTemplate = `{ "id": { "type": "integer" }, + "reactions": { + "$ref": "#/definitions/models.ReactionMap" + }, "updated": { "type": "string" } @@ -9190,6 +9211,8 @@ var SwaggerInfo = &swag.Spec{ Description: "# Pagination\nEvery endpoint capable of pagination will return two headers:\n* `x-pagination-total-pages`: The total number of available pages for this request\n* `x-pagination-result-count`: The number of items returned for this request.\n# Rights\nAll endpoints which return a single item (project, task, etc.) - no array - will also return a `x-max-right` header with the max right the user has on this item as an int where `0` is `Read Only`, `1` is `Read & Write` and `2` is `Admin`.\nThis can be used to show or hide ui elements based on the rights the user has.\n# Errors\nAll errors have an error code and a human-readable error message in addition to the http status code. You should always check for the status code in the response, not only the http status code.\nDue to limitations in the swagger library we're using for this document, only one error per http status code is documented here. Make sure to check the [error docs](https://vikunja.io/docs/errors/) in Vikunja's documentation for a full list of available error codes.\n# Authorization\n**JWT-Auth:** Main authorization method, used for most of the requests. Needs `Authorization: Bearer `-header to authenticate successfully.\n\n**API Token:** You can create scoped API tokens for your user and use the token to make authenticated requests in the context of that user. The token must be provided via an `Authorization: Bearer ` header, similar to jwt auth. See the documentation for the `api` group to manage token creation and revocation.\n\n**BasicAuth:** Only used when requesting tasks via CalDAV.\n", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", } func init() { diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index b8624844f..d5d89a40b 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -7098,6 +7098,7 @@ "JWTKeyAuth": [] } ], + "description": "Add a reaction to an entity. Will do nothing if the reaction already exists.", "consumes": [ "application/json" ], @@ -7153,8 +7154,10 @@ } } } - }, - "delete": { + } + }, + "/{kind}/{id}/reactions/delete": { + "post": { "security": [ { "JWTKeyAuth": [] @@ -7582,6 +7585,14 @@ "description": "The project this task belongs to.", "type": "integer" }, + "reactions": { + "description": "Reactions on that task.", + "allOf": [ + { + "$ref": "#/definitions/models.ReactionMap" + } + ] + }, "related_tasks": { "description": "All related tasks, grouped by their relation kind", "allOf": [ @@ -8230,6 +8241,14 @@ "description": "The project this task belongs to.", "type": "integer" }, + "reactions": { + "description": "Reactions on that task.", + "allOf": [ + { + "$ref": "#/definitions/models.ReactionMap" + } + ] + }, "related_tasks": { "description": "All related tasks, grouped by their relation kind", "allOf": [ @@ -8353,6 +8372,9 @@ "id": { "type": "integer" }, + "reactions": { + "$ref": "#/definitions/models.ReactionMap" + }, "updated": { "type": "string" } diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 1accd27a6..9a9f34822 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -237,6 +237,10 @@ definitions: project_id: description: The project this task belongs to. type: integer + reactions: + allOf: + - $ref: '#/definitions/models.ReactionMap' + description: Reactions on that task. related_tasks: allOf: - $ref: '#/definitions/models.RelatedTaskMap' @@ -743,6 +747,10 @@ definitions: project_id: description: The project this task belongs to. type: integer + reactions: + allOf: + - $ref: '#/definitions/models.ReactionMap' + description: Reactions on that task. related_tasks: allOf: - $ref: '#/definitions/models.RelatedTaskMap' @@ -834,6 +842,8 @@ definitions: type: string id: type: integer + reactions: + $ref: '#/definitions/models.ReactionMap' updated: type: string type: object @@ -1461,48 +1471,6 @@ info: title: Vikunja API paths: /{kind}/{id}/reactions: - delete: - consumes: - - application/json - description: Removes the reaction of that user on that entity. - parameters: - - description: Entity ID - in: path - name: id - required: true - type: integer - - description: The kind of the entity. Can be either `tasks` or `comments` for - task comments - in: path - name: kind - required: true - type: integer - - description: The reaction you want to add to the entity. - in: body - name: project - required: true - schema: - $ref: '#/definitions/models.Reaction' - produces: - - application/json - responses: - "200": - description: The reaction was successfully removed. - schema: - $ref: '#/definitions/models.Message' - "403": - description: The user does not have access to the entity - schema: - $ref: '#/definitions/web.HTTPError' - "500": - description: Internal error - schema: - $ref: '#/definitions/models.Message' - security: - - JWTKeyAuth: [] - summary: Removes the user's reaction - tags: - - task get: consumes: - application/json @@ -1544,6 +1512,8 @@ paths: put: consumes: - application/json + description: Add a reaction to an entity. Will do nothing if the reaction already + exists. parameters: - description: Entity ID in: path @@ -1582,6 +1552,49 @@ paths: summary: Add a reaction to an entity tags: - task + /{kind}/{id}/reactions/delete: + post: + consumes: + - application/json + description: Removes the reaction of that user on that entity. + parameters: + - description: Entity ID + in: path + name: id + required: true + type: integer + - description: The kind of the entity. Can be either `tasks` or `comments` for + task comments + in: path + name: kind + required: true + type: integer + - description: The reaction you want to add to the entity. + in: body + name: project + required: true + schema: + $ref: '#/definitions/models.Reaction' + produces: + - application/json + responses: + "200": + description: The reaction was successfully removed. + schema: + $ref: '#/definitions/models.Message' + "403": + description: The user does not have access to the entity + schema: + $ref: '#/definitions/web.HTTPError' + "500": + description: Internal error + schema: + $ref: '#/definitions/models.Message' + security: + - JWTKeyAuth: [] + summary: Removes the user's reaction + tags: + - task /{username}/avatar: get: description: Returns the user avatar as image. From 0e2ad5dde6382dc8134f91f53af47429501fd087 Mon Sep 17 00:00:00 2001 From: renovate Date: Tue, 12 Mar 2024 20:07:21 +0000 Subject: [PATCH 27/66] fix(deps): pin dependency vuemoji-picker to 0.2.1 --- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 424c68f46..ddf6a1aa2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -123,7 +123,7 @@ "vue-flatpickr-component": "11.0.5", "vue-i18n": "9.10.1", "vue-router": "4.3.0", - "vuemoji-picker": "^0.2.1", + "vuemoji-picker": "0.2.1", "workbox-precaching": "7.0.0", "zhyswan-vuedraggable": "4.1.3" }, diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index ff01839a5..02b83faa3 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -233,7 +233,7 @@ dependencies: specifier: 4.3.0 version: 4.3.0(vue@3.4.21) vuemoji-picker: - specifier: ^0.2.1 + specifier: 0.2.1 version: 0.2.1(vue@3.4.21) workbox-precaching: specifier: 7.0.0 From e44897e0d4aaf0cc3dec9895e829c6950f00efd8 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 12 Mar 2024 21:28:31 +0100 Subject: [PATCH 28/66] fix(filter): do not match join operator Partial fix for https://kolaente.dev/vikunja/vikunja/issues/2194 --- .../project/partials/FilterInput.vue | 19 +++++++++++-------- frontend/src/helpers/filters.ts | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/project/partials/FilterInput.vue b/frontend/src/components/project/partials/FilterInput.vue index e02eb6838..d668fde47 100644 --- a/frontend/src/components/project/partials/FilterInput.vue +++ b/frontend/src/components/project/partials/FilterInput.vue @@ -10,13 +10,15 @@ import User from '@/components/misc/user.vue' import ProjectUserService from '@/services/projectUsers' import {useProjectStore} from '@/stores/projects' import { - DATE_FIELDS, ASSIGNEE_FIELDS, AUTOCOMPLETE_FIELDS, AVAILABLE_FILTER_FIELDS, + DATE_FIELDS, FILTER_JOIN_OPERATOR, FILTER_OPERATORS, - FILTER_OPERATORS_REGEX, LABEL_FIELDS, getFilterFieldRegexPattern, + FILTER_OPERATORS_REGEX, + getFilterFieldRegexPattern, + LABEL_FIELDS, } from '@/helpers/filters' const { @@ -110,9 +112,9 @@ const highlightedFilterQuery = computed(() => { if (typeof value === 'undefined') { value = '' } - - let labelTitles = [value] - if(operator === 'in' || operator === '?=') { + + let labelTitles = [value.trim()] + if (operator === 'in' || operator === '?=') { labelTitles = value.split(',').map(v => v.trim()) } @@ -122,7 +124,8 @@ const highlightedFilterQuery = computed(() => { labelsHtml.push(`${label?.title ?? t}`) }) - return `${f} ${operator} ${labelsHtml.join(', ')}` + const endSpace = value.endsWith(' ') ? ' ' : '' + return `${f} ${operator} ${labelsHtml.join(', ')}${endSpace}` }) }) FILTER_OPERATORS @@ -195,7 +198,7 @@ function handleFieldInput() { const [matched, prefix, operator, space, keyword] = match if (keyword) { let search = keyword - if(operator === 'in' || operator === '?=') { + if (operator === 'in' || operator === '?=') { const keywords = keyword.split(',') search = keywords[keywords.length - 1].trim() } @@ -348,7 +351,7 @@ function autocompleteSelect(value) { resize: none; text-fill-color: transparent; -webkit-text-fill-color: transparent; - + &::placeholder { text-fill-color: var(--input-placeholder-color); -webkit-text-fill-color: var(--input-placeholder-color); diff --git a/frontend/src/helpers/filters.ts b/frontend/src/helpers/filters.ts index de10f57f6..4a15696e3 100644 --- a/frontend/src/helpers/filters.ts +++ b/frontend/src/helpers/filters.ts @@ -58,7 +58,7 @@ export const FILTER_JOIN_OPERATOR = [ export const FILTER_OPERATORS_REGEX = '(<|>|<=|>=|=|!=|in)' export function getFilterFieldRegexPattern(field: string): RegExp { - return new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&|()]+\\1?)?', 'ig') + return new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&|()<]+\\1?)?', 'ig') } export function transformFilterStringForApi( From eb4f880c64e9a4a3e2eb521144630e4901af09e4 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 12 Mar 2024 21:30:54 +0100 Subject: [PATCH 29/66] fix(filter): do not show filter footer when creating a filter --- frontend/src/views/filters/FilterNew.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/views/filters/FilterNew.vue b/frontend/src/views/filters/FilterNew.vue index 0a5baf1a1..67f8642f6 100644 --- a/frontend/src/views/filters/FilterNew.vue +++ b/frontend/src/views/filters/FilterNew.vue @@ -62,6 +62,7 @@ :class="{ 'disabled': filterService.loading}" :disabled="filterService.loading" class="has-no-shadow has-no-border" + :has-footer="false" />
From cf6b476b7d750265026fde462128eb65a8cbff78 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 12 Mar 2024 21:33:24 +0100 Subject: [PATCH 30/66] chore: cleanup leftover console.log --- frontend/src/services/abstractService.ts | 1 - frontend/src/stores/tasks.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/frontend/src/services/abstractService.ts b/frontend/src/services/abstractService.ts index 820347618..f95723056 100644 --- a/frontend/src/services/abstractService.ts +++ b/frontend/src/services/abstractService.ts @@ -392,7 +392,6 @@ export default abstract class AbstractService { const taskService = new TaskService() try { const updatedTask = await taskService.update(task) - console.log({updated: updatedTask.reactions, old: task.reactions}) kanbanStore.setTaskInBucket(updatedTask) return updatedTask } finally { From e1c972d64d8e614b3c423a1644e6d9e01e0f8b28 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 12 Mar 2024 22:05:26 +0100 Subject: [PATCH 31/66] fix(filters): replace project titles at the match position, not anywhere in the filter string This fixes a bug where the project title would not be replaced correctly in cases where the project title contained parts of the word "project". Resolves https://kolaente.dev/vikunja/vikunja/issues/2194 --- frontend/src/helpers/filters.test.ts | 14 ++++++++++++-- frontend/src/helpers/filters.ts | 9 ++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/frontend/src/helpers/filters.test.ts b/frontend/src/helpers/filters.test.ts index 6f04f5757..2a71d9f9f 100644 --- a/frontend/src/helpers/filters.test.ts +++ b/frontend/src/helpers/filters.test.ts @@ -18,7 +18,7 @@ describe('Filter Transformation', () => { 'labels': 'labels', } - describe('For api', () => { + describe('For API', () => { for (const c in fieldCases) { it('should transform all filter params for ' + c + ' to snake_case', () => { const transformed = transformFilterStringForApi(c + ' = ipsum', nullTitleToIdResolver, nullTitleToIdResolver) @@ -97,6 +97,16 @@ describe('Filter Transformation', () => { expect(transformed).toBe('project in 1, 2') }) + + it('should resolve projects at the correct position', () => { + const transformed = transformFilterStringForApi( + 'project = pr', + nullTitleToIdResolver, + (title: string) => 1, + ) + + expect(transformed).toBe('project = 1') + }) }) describe('To API', () => { @@ -138,7 +148,7 @@ describe('Filter Transformation', () => { expect(transformed).toBe('labels = lorem && dueDate = now && labels = ipsum') }) - + it('should correctly resolve multiple labels in', () => { const transformed = transformFilterStringFromApi( 'labels in 1, 2', diff --git a/frontend/src/helpers/filters.ts b/frontend/src/helpers/filters.ts index 4a15696e3..7f9a39e10 100644 --- a/frontend/src/helpers/filters.ts +++ b/frontend/src/helpers/filters.ts @@ -108,12 +108,19 @@ export function transformFilterStringForApi( keywords = keyword.trim().split(',').map(k => k.trim()) } + let replaced = keyword + keywords.forEach(k => { const projectId = projectResolver(k) if (projectId !== null) { - filter = filter.replace(k, String(projectId)) + replaced = replaced.replace(k, String(projectId)) } }) + + const actualKeywordStart = (match?.index || 0) + prefix.length + filter = filter.substring(0, actualKeywordStart) + + replaced + + filter.substring(actualKeywordStart + keyword.length) } } }) From 5b2b7f7bdc2e03f011bcee5404ef8308d689dcc3 Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 12 Mar 2024 22:23:35 +0100 Subject: [PATCH 32/66] fix(kanban): reset done and default bucket when the bucket itself is deleted Resolves https://github.com/go-vikunja/vikunja/issues/234 --- pkg/models/kanban.go | 30 +++++++++++++++++++++++------- pkg/models/kanban_test.go | 17 +++++++++++++++++ 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/pkg/models/kanban.go b/pkg/models/kanban.go index 597420a97..0b6adaffc 100644 --- a/pkg/models/kanban.go +++ b/pkg/models/kanban.go @@ -310,7 +310,7 @@ func (b *Bucket) Update(s *xorm.Session, _ web.Auth) (err error) { // @Failure 404 {object} web.HTTPError "The bucket does not exist." // @Failure 500 {object} models.Message "Internal error" // @Router /projects/{projectID}/buckets/{bucketID} [delete] -func (b *Bucket) Delete(s *xorm.Session, _ web.Auth) (err error) { +func (b *Bucket) Delete(s *xorm.Session, a web.Auth) (err error) { // Prevent removing the last bucket total, err := s.Where("project_id = ?", b.ProjectID).Count(&Bucket{}) @@ -324,17 +324,27 @@ func (b *Bucket) Delete(s *xorm.Session, _ web.Auth) (err error) { } } - // Remove the bucket itself - _, err = s.Where("id = ?", b.ID).Delete(&Bucket{}) - if err != nil { - return - } - // Get the default bucket p, err := GetProjectSimpleByID(s, b.ProjectID) if err != nil { return } + var updateProject bool + if b.ID == p.DefaultBucketID { + p.DefaultBucketID = 0 + updateProject = true + } + if b.ID == p.DoneBucketID { + p.DoneBucketID = 0 + updateProject = true + } + if updateProject { + err = p.Update(s, a) + if err != nil { + return + } + } + defaultBucketID, err := getDefaultBucketID(s, p) if err != nil { return err @@ -345,5 +355,11 @@ func (b *Bucket) Delete(s *xorm.Session, _ web.Auth) (err error) { Where("bucket_id = ?", b.ID). Cols("bucket_id"). Update(&Task{BucketID: defaultBucketID}) + if err != nil { + return + } + + // Remove the bucket itself + _, err = s.Where("id = ?", b.ID).Delete(&Bucket{}) return } diff --git a/pkg/models/kanban_test.go b/pkg/models/kanban_test.go index e972b126b..c997d9d18 100644 --- a/pkg/models/kanban_test.go +++ b/pkg/models/kanban_test.go @@ -197,6 +197,23 @@ func TestBucket_Delete(t *testing.T) { "project_id": 18, }, false) }) + t.Run("done bucket should be reset", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + b := &Bucket{ + ID: 3, + ProjectID: 1, + } + err := b.Delete(s, user) + require.NoError(t, err) + + db.AssertMissing(t, "projects", map[string]interface{}{ + "id": 1, + "done_bucket_id": 3, + }) + }) } func TestBucket_Update(t *testing.T) { From fb5b2542a5774c59e0d93798f3afb2fbb94ab6ac Mon Sep 17 00:00:00 2001 From: "Frederick [Bot]" Date: Wed, 13 Mar 2024 00:05:52 +0000 Subject: [PATCH 33/66] chore(i18n): update translations via Crowdin --- frontend/src/i18n/lang/ar-SA.json | 74 ++++++++++++++++++++- frontend/src/i18n/lang/ca-ES.json | 74 ++++++++++++++++++++- frontend/src/i18n/lang/cs-CZ.json | 90 +++++++++++++++++++++++--- frontend/src/i18n/lang/da-DK.json | 74 ++++++++++++++++++++- frontend/src/i18n/lang/de-DE.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/de-swiss.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/eo-UY.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/es-ES.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/fr-FR.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/hu-HU.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/it-IT.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/ja-JP.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/ko-KR.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/nl-NL.json | 74 ++++++++++++++++++++- frontend/src/i18n/lang/no-NO.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/pl-PL.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/pt-BR.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/pt-PT.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/ro-RO.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/ru-RU.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/sk-SK.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/sl-SI.json | 96 +++++++++++++++++++++++++--- frontend/src/i18n/lang/sr-CS.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/sv-SE.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/tr-TR.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/vi-VN.json | 80 ++++++++++++++++++++++- frontend/src/i18n/lang/zh-CN.json | 74 ++++++++++++++++++++- frontend/src/i18n/lang/zh-TW.json | 74 ++++++++++++++++++++- 28 files changed, 2186 insertions(+), 44 deletions(-) diff --git a/frontend/src/i18n/lang/ar-SA.json b/frontend/src/i18n/lang/ar-SA.json index 813ded57e..79b5682fa 100644 --- a/frontend/src/i18n/lang/ar-SA.json +++ b/frontend/src/i18n/lang/ar-SA.json @@ -248,6 +248,7 @@ "text2": "هذا يشمل جميع المهام ولا يمكن التراجع عن هذا الإجراء!", "success": "تم حذف المشروع بنجاح.", "tasksToDelete": "سيؤدي هذا إلى حذف ما يقارب {count} من المهام.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "هذا المشروع لا يحتوي على أي مهام، يمكن حذفه بشكل آمن." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "الفلاتر", "clear": "مسح الفلاتر", + "showResults": "Show results", "attributes": { "title": "العنوان", "titlePlaceholder": "عنوان الفلتر المحفوظ هنا…", @@ -415,6 +417,52 @@ "edit": { "title": "تعديل هذا الفلتر المحفوظ", "success": "تم حفظ الفلتر بنجاح." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "إلى", "from": "من", "fromto": "{from} إلى {to}", + "date": "Date", "ranges": { "today": "اليوم", "thisWeek": "هذا الأسبوع", @@ -598,6 +647,27 @@ "lastMonth": "الشهر الماضي", "thisYear": "هذه السنة", "restOfThisYear": "المتبقي من هذه السنة" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "الوصف", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "مشرف", - "member": "عضو" + "member": "عضو", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { diff --git a/frontend/src/i18n/lang/ca-ES.json b/frontend/src/i18n/lang/ca-ES.json index 3c444813e..2028930a3 100644 --- a/frontend/src/i18n/lang/ca-ES.json +++ b/frontend/src/i18n/lang/ca-ES.json @@ -248,6 +248,7 @@ "text2": "This includes all tasks and CANNOT BE UNDONE!", "success": "The project was successfully deleted.", "tasksToDelete": "This will irrevocably remove approx. {count} tasks.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "This project does not contain any tasks, it should be safe to delete." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filters", "clear": "Clear Filters", + "showResults": "Show results", "attributes": { "title": "Title", "titlePlaceholder": "The saved filter title goes here…", @@ -415,6 +417,52 @@ "edit": { "title": "Edit This Saved Filter", "success": "The filter was saved successfully." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "To", "from": "From", "fromto": "{from} to {to}", + "date": "Date", "ranges": { "today": "Today", "thisWeek": "This Week", @@ -598,6 +647,27 @@ "lastMonth": "Last Month", "thisYear": "This Year", "restOfThisYear": "The Rest of This Year" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Description", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "Admin", - "member": "Member" + "member": "Member", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { diff --git a/frontend/src/i18n/lang/cs-CZ.json b/frontend/src/i18n/lang/cs-CZ.json index 8447dfa5c..f91a44be3 100644 --- a/frontend/src/i18n/lang/cs-CZ.json +++ b/frontend/src/i18n/lang/cs-CZ.json @@ -57,11 +57,11 @@ "logout": "Odhlásit se", "emailInvalid": "Prosím zadejte platnou emailovou adresu.", "usernameRequired": "Zadejte prosím uživatelské jméno.", - "usernameMustNotContainSpace": "The username must not contain spaces.", - "usernameMustNotLookLikeUrl": "The username must not look like a URL.", + "usernameMustNotContainSpace": "Uživatelské jméno nesmí obsahovat mezery.", + "usernameMustNotLookLikeUrl": "Uživatelské jméno nesmí vypadat jako adresa URL.", "passwordRequired": "Zadejte prosím heslo.", - "passwordNotMin": "Password must have at least 8 characters.", - "passwordNotMax": "Password must have at most 250 characters.", + "passwordNotMin": "Heslo musí mít nejméně 8 znaků.", + "passwordNotMax": "Heslo může mít maximálně 250 znaků.", "showPassword": "Ukázat heslo", "hidePassword": "Skrýt heslo", "noAccountYet": "Ještě nemáte účet?", @@ -248,6 +248,7 @@ "text2": "To zahrnuje všechny úkoly a JE TO NEVRATNÉ!", "success": "Projekt byl úspěšně smazán.", "tasksToDelete": "Neodvolatelně tím odstraníme asi {count} úloh.", + "tasksAndChildProjectsToDelete": "Chystáte se neodvolatelně odstranit nějaké úkoly (cca {tasks}) a projekty (cca {projects}).", "noTasksToDelete": "Tento projekt neobsahuje žádné úkoly, mělo by být bezpečné ho smazat." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filtry", "clear": "Vymazat filtry", + "showResults": "Zobrazit výsledky", "attributes": { "title": "Název", "titlePlaceholder": "Název uloženého filtru přijde sem…", @@ -415,6 +417,52 @@ "edit": { "title": "Upravit tento uložený filtr", "success": "Filtr byl úspěšně uložen." + }, + "query": { + "title": "Dotaz", + "placeholder": "Zadejte vyhledávací nebo filtrační dotaz…", + "help": { + "intro": "K filtrování úkolů můžete použít syntaxi dotazů podobnou SQL. Dostupná pole pro filtrování zahrnují:", + "link": "Jak to funguje?", + "canUseDatemath": "Můžete použít matematiku a nastavit relativní data. Pro více informací klikněte na datum v dotazu.", + "fields": { + "done": "Zda je úkol dokončen nebo ne", + "priority": "Úroveň priority úkolu (1-5)", + "percentDone": "Procento dokončení úkolu (0-100)", + "dueDate": "Datum dokončení úkolu", + "startDate": "Datum zahájení úkolu", + "endDate": "Datum dokončení úkolu", + "doneAt": "Datum a čas, kdy byl úkol dokončen", + "assignees": "Přiřazení uživatelé", + "labels": "Štítky přiřazené úkolu", + "project": "Projekt, do kterého úkol patří (k dispozici pouze pro uložené filtry, ne na úrovni projektu)" + }, + "operators": { + "intro": "Dostupné operátory pro filtrování zahrnují:", + "notEqual": "Nerovná se", + "equal": "Rovná se", + "greaterThan": "Větší než", + "greaterThanOrEqual": "Větší nebo rovno než", + "lessThan": "Menší než", + "lessThanOrEqual": "Menší nebo rovno než", + "like": "Odpovídá vzoru (s použitím zástupného znaku %)", + "in": "Odpovídá libovolné hodnotě v seznamu hodnot oddělených čárkou" + }, + "logicalOperators": { + "intro": "Pro kombinování více podmínek můžete použít tyto logické operátory:", + "and": "AND - shoduje se, pokud jsou splněny všechny podmínky", + "or": "OR - shoduje se, pokud je splněna alespoň jedna podmínka", + "parentheses": "Závorky seskupují podmínky" + }, + "examples": { + "intro": "Zde jsou některé příklady filtrovacích dotazů:", + "priorityEqual": "Odpovídá úkolům s úrovní priority 4", + "dueDatePast": "Odpovídá úkolům s termínem dokončení v minulosti", + "undoneHighPriority": "Odpovídá nedokončeným úkolům s úrovní priority 3 nebo vyšší", + "assigneesIn": "Odpovídá úkolům přiřazeným buď \"uživatel1\", nebo \"uživatel2\"", + "priorityOneOrTwoPastDue": "Odpovídá úkolům s prioritou 1 nebo 2 a termínem dokončení v minulosti" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "Do", "from": "Od", "fromto": "{from} – {to}", + "date": "Datum", "ranges": { "today": "Dnes", "thisWeek": "Tento týden", @@ -598,6 +647,27 @@ "lastMonth": "Minulý měsíc", "thisYear": "Tento rok", "restOfThisYear": "Zbytek tohoto roku" + }, + "values": { + "now": "Teď", + "startOfToday": "Začátek dnes", + "endOfToday": "Konec dnes", + "beginningOflastWeek": "Začátek minulého týdne", + "endOfLastWeek": "Konec posledního týdne", + "beginningOfThisWeek": "Začátek tohoto týdne", + "endOfThisWeek": "Konec tohoto týdne", + "startOfNextWeek": "Začátek příštího týdne", + "endOfNextWeek": "Konec tohoto týdne", + "in7Days": "Za 7 dní", + "beginningOfLastMonth": "Začátek minulého měsíce", + "endOfLastMonth": "Konec minulého měsíce", + "startOfThisMonth": "Začátek tohoto měsíce", + "endOfThisMonth": "Konec tohoto měsíce", + "startOfNextMonth": "Začátek příštího měsíce", + "endOfNextMonth": "Konec příštího měsíce", + "in30Days": "Za 30 dní", + "startOfThisYear": "Začátek tohoto roku", + "endOfThisYear": "Konec tohoto roku" } }, "datemathHelp": { @@ -713,7 +783,7 @@ "startDate": "Počáteční datum", "title": "Název", "updated": "Aktualizováno", - "doneAt": "Done At" + "doneAt": "Dokončeno" }, "subscription": { "subscribedTaskThroughParentProject": "Zde se nemůžete odhlásit, protože jste přihlášeni k odběru tohoto úkolu prostřednictvím jeho projektu.", @@ -916,7 +986,9 @@ "description": "Popis", "descriptionPlaceholder": "Popište tým, stiskněte '/' pro více možností…", "admin": "Administrátor", - "member": "Člen" + "member": "Člen", + "isPublic": "Veřejný tým", + "isPublicDescription": "Učinit tým veřejně dostupným. Pokud je povoleno, každý může s tímto týmem sdílet projekty, i když není přímým členem." } }, "keyboardShortcuts": { @@ -975,8 +1047,8 @@ "share": "Sdílet", "newProject": "Nový projekt", "createProject": "Vytvořit projekt", - "cantArchiveIsDefault": "You cannot archive this because it is your default project.", - "cantDeleteIsDefault": "You cannot delete this because it is your default project." + "cantArchiveIsDefault": "Nemůžete archivovat svůj výchozí projekt.", + "cantDeleteIsDefault": "Nemůžete smazat svůj výchozí projekt." }, "apiConfig": { "url": "Vikunja URL", @@ -1096,7 +1168,7 @@ }, "about": { "title": "O aplikaci", - "version": "Version: {version}" + "version": "Verze: {version}" }, "time": { "units": { diff --git a/frontend/src/i18n/lang/da-DK.json b/frontend/src/i18n/lang/da-DK.json index 73021a2d1..ab9bd8a9e 100644 --- a/frontend/src/i18n/lang/da-DK.json +++ b/frontend/src/i18n/lang/da-DK.json @@ -248,6 +248,7 @@ "text2": "This includes all tasks and CANNOT BE UNDONE!", "success": "The project was successfully deleted.", "tasksToDelete": "This will irrevocably remove approx. {count} tasks.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "This project does not contain any tasks, it should be safe to delete." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filtre", "clear": "Ryd Filtre", + "showResults": "Show results", "attributes": { "title": "Titel", "titlePlaceholder": "Det gemte filters titel skrives her…", @@ -415,6 +417,52 @@ "edit": { "title": "Rediger Dette Gemte Filter", "success": "Filteret blev slettet." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "Til", "from": "Fra", "fromto": "{from} til {to}", + "date": "Date", "ranges": { "today": "I dag", "thisWeek": "Denne uge", @@ -598,6 +647,27 @@ "lastMonth": "Sidste måned", "thisYear": "Dette år", "restOfThisYear": "Resten af dette år" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Beskrivelse", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "Administrator", - "member": "Medlem" + "member": "Medlem", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { diff --git a/frontend/src/i18n/lang/de-DE.json b/frontend/src/i18n/lang/de-DE.json index 92969b72f..78a806353 100644 --- a/frontend/src/i18n/lang/de-DE.json +++ b/frontend/src/i18n/lang/de-DE.json @@ -248,6 +248,7 @@ "text2": "Dies umfasst alle Aufgaben und kann NICHT rückgängig gemacht werden!", "success": "Das Projekt wurde erfolgreich gelöscht.", "tasksToDelete": "Dies löscht unwiderruflich ca. {count} Aufgaben.", + "tasksAndChildProjectsToDelete": "Dies löscht unwiderruflich ca. {tasks} Aufgaben und {projects} Projekte.", "noTasksToDelete": "Dieses Projekt enthält keine Aufgaben, es kann sicher gelöscht werden." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filter", "clear": "Filter zurücksetzen", + "showResults": "Ergebnisse anzeigen", "attributes": { "title": "Titel", "titlePlaceholder": "Einen gespeicherten Filternamen eingeben …", @@ -415,6 +417,52 @@ "edit": { "title": "Diesen gespeicherten Filter bearbeiten", "success": "Filter gespeichert." + }, + "query": { + "title": "Abfrage", + "placeholder": "Gib eine Suche oder Filterabfrage ein…", + "help": { + "intro": "Um Aufgaben zu filtern, kannst du eine SQL-ähnliche Abfragesyntax verwenden. Du kannst die folgenden Felder in deinem Filter verwenden:", + "link": "Wie funktioniert das?", + "canUseDatemath": "Du kannst Date Math verwenden, um relative Daten festzulegen. Klicke auf den Datumswert in einer Abfrage, um mehr zu erfahren.", + "fields": { + "done": "Ob die Aufgabe erledigt ist oder nicht", + "priority": "Die Priorität der Aufgabe (1-5)", + "percentDone": "Prozentsatz der Fertigstellung der Aufgabe (0-100)", + "dueDate": "Das Fälligkeitsdatum der Aufgabe", + "startDate": "Das Startdatum der Aufgabe", + "endDate": "Das Enddatum der Aufgabe", + "doneAt": "Datum und Uhrzeit, an dem die Aufgabe als erledigt markiert wurde", + "assignees": "Die der Aufgabe Zugewiesenen", + "labels": "Die der Aufgabe zugeordneten Labels", + "project": "Das Projekt, zu dem die Aufgabe gehört (nur verfügbar für gespeicherte Filter, nicht auf Projektebene)" + }, + "operators": { + "intro": "Die verfügbaren Operatoren für die Filterung sind:", + "notEqual": "Ungleich", + "equal": "Gleich", + "greaterThan": "Größer als", + "greaterThanOrEqual": "Größer oder gleich", + "lessThan": "Kleiner als", + "lessThanOrEqual": "Kleiner als oder gleich", + "like": "Vergleicht zu einem Muster (mit Platzhalter %)", + "in": "Filtert einen beliebigen Wert in einer kommaseparierten Liste" + }, + "logicalOperators": { + "intro": "Um mehrere Bedingungen zu kombinieren, kannst du folgende logische Operatoren verwenden:", + "and": "UND Operator, stimmt überein, wenn alle Bedingungen wahr sind", + "or": "ODER Operator, stimmt überein, wenn eine der Bedingungen wahr ist", + "parentheses": "Klammern zum Gruppieren von Bedingungen" + }, + "examples": { + "intro": "Hier sind einige Beispiele für Filterabfragen:", + "priorityEqual": "Findet Aufgaben mit Priorität Level 4", + "dueDatePast": "Findet Aufgaben mit einem Fälligkeitsdatum in der Vergangenheit", + "undoneHighPriority": "Findet Aufgaben, die nicht erledigt sind und Priorität Level mindestens 3 haben", + "assigneesIn": "Findet Aufgaben, die entweder \"user1\" oder \"user2\" zugewiesen sind", + "priorityOneOrTwoPastDue": "Findet Aufgaben mit Priorität Level 1 oder 2 und einem Fälligkeitsdatum in der Vergangenheit" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "Bis", "from": "Von", "fromto": "{from} bis {to}", + "date": "Datum", "ranges": { "today": "Heute", "thisWeek": "Diese Woche", @@ -598,6 +647,27 @@ "lastMonth": "Letzter Monat", "thisYear": "Dieses Jahr", "restOfThisYear": "Der Rest des Jahres" + }, + "values": { + "now": "Jetzt", + "startOfToday": "Beginn des heutigen Tages", + "endOfToday": "Ende des heutigen Tages", + "beginningOflastWeek": "Beginn der letzten Woche", + "endOfLastWeek": "Ende der letzten Woche", + "beginningOfThisWeek": "Beginn dieser Woche", + "endOfThisWeek": "Ende dieser Woche", + "startOfNextWeek": "Beginn der nächsten Woche", + "endOfNextWeek": "Ende der nächsten Woche", + "in7Days": "In 7 Tagen", + "beginningOfLastMonth": "Beginn des letzten Monats", + "endOfLastMonth": "Ende des letzten Monats", + "startOfThisMonth": "Beginn diesen Monats", + "endOfThisMonth": "Ende diesen Monats", + "startOfNextMonth": "Beginn des nächsten Monats", + "endOfNextMonth": "Ende des nächsten Monats", + "in30Days": "In 30 Tagen", + "startOfThisYear": "Beginn dieses Jahres", + "endOfThisYear": "Ende dieses Jahres" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Beschreibung", "descriptionPlaceholder": "Gib eine Beschreibung für dieses Team ein, drücke '/' für mehr Optionen…", "admin": "Admin", - "member": "Mitglied" + "member": "Mitglied", + "isPublic": "Öffentliches Team", + "isPublicDescription": "Machs das Team öffentlich sichtbar. Wenn aktiviert, kann jede:r mit diesem Team Projekte teilen, auch wenn er oder sie kein direktes Mitglied des Teams ist." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "j M Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Fehler", "success": "Erfolgreich", diff --git a/frontend/src/i18n/lang/de-swiss.json b/frontend/src/i18n/lang/de-swiss.json index 5ac9d32f7..15da13b8f 100644 --- a/frontend/src/i18n/lang/de-swiss.json +++ b/frontend/src/i18n/lang/de-swiss.json @@ -248,6 +248,7 @@ "text2": "Dies umfasst alle Aufgaben und kann NICHT rückgängig gemacht werden!", "success": "Das Projekt wurde erfolgreich gelöscht.", "tasksToDelete": "Dies löscht unwiderruflich ca. {count} Aufgaben.", + "tasksAndChildProjectsToDelete": "Dies löscht unwiderruflich ca. {tasks} Aufgaben und {projects} Projekte.", "noTasksToDelete": "Dieses Projekt enthält keine Aufgaben, es kann sicher gelöscht werden." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filter", "clear": "Filter zurücksetzen", + "showResults": "Ergebnisse anzeigen", "attributes": { "title": "Titl", "titlePlaceholder": "De Name für de g'speicheret Filter chunt da ahne…", @@ -415,6 +417,52 @@ "edit": { "title": "De g'speicheret Filter bearbeite", "success": "De filter isch erfolgriich g'speichered wore." + }, + "query": { + "title": "Abfrage", + "placeholder": "Gib eine Suche oder Filterabfrage ein…", + "help": { + "intro": "Um Aufgaben zu filtern, kannst du eine SQL-ähnliche Abfragesyntax verwenden. Du kannst die folgenden Felder in deinem Filter verwenden:", + "link": "Wie funktioniert das?", + "canUseDatemath": "Du kannst Date Math verwenden, um relative Daten festzulegen. Klicke auf den Datumswert in einer Abfrage, um mehr zu erfahren.", + "fields": { + "done": "Ob die Aufgabe erledigt ist oder nicht", + "priority": "Die Priorität der Aufgabe (1-5)", + "percentDone": "Prozentsatz der Fertigstellung der Aufgabe (0-100)", + "dueDate": "Das Fälligkeitsdatum der Aufgabe", + "startDate": "Das Startdatum der Aufgabe", + "endDate": "Das Enddatum der Aufgabe", + "doneAt": "Datum und Uhrzeit, an dem die Aufgabe als erledigt markiert wurde", + "assignees": "Die der Aufgabe Zugewiesenen", + "labels": "Die der Aufgabe zugeordneten Labels", + "project": "Das Projekt, zu dem die Aufgabe gehört (nur verfügbar für gespeicherte Filter, nicht auf Projektebene)" + }, + "operators": { + "intro": "Die verfügbaren Operatoren für die Filterung sind:", + "notEqual": "Ungleich", + "equal": "Gleich", + "greaterThan": "Größer als", + "greaterThanOrEqual": "Größer oder gleich", + "lessThan": "Kleiner als", + "lessThanOrEqual": "Kleiner als oder gleich", + "like": "Vergleicht zu einem Muster (mit Platzhalter %)", + "in": "Filtert einen beliebigen Wert in einer kommaseparierten Liste" + }, + "logicalOperators": { + "intro": "Um mehrere Bedingungen zu kombinieren, kannst du folgende logische Operatoren verwenden:", + "and": "UND Operator, stimmt überein, wenn alle Bedingungen wahr sind", + "or": "ODER Operator, stimmt überein, wenn eine der Bedingungen wahr ist", + "parentheses": "Klammern zum Gruppieren von Bedingungen" + }, + "examples": { + "intro": "Hier sind einige Beispiele für Filterabfragen:", + "priorityEqual": "Findet Aufgaben mit Priorität Level 4", + "dueDatePast": "Findet Aufgaben mit einem Fälligkeitsdatum in der Vergangenheit", + "undoneHighPriority": "Findet Aufgaben, die nicht erledigt sind und Priorität Level mindestens 3 haben", + "assigneesIn": "Findet Aufgaben, die entweder \"user1\" oder \"user2\" zugewiesen sind", + "priorityOneOrTwoPastDue": "Findet Aufgaben mit Priorität Level 1 oder 2 und einem Fälligkeitsdatum in der Vergangenheit" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "Bis", "from": "Von", "fromto": "{from} bis {to}", + "date": "Datum", "ranges": { "today": "Heute", "thisWeek": "Diese Woche", @@ -598,6 +647,27 @@ "lastMonth": "Letzter Monat", "thisYear": "Dieses Jahr", "restOfThisYear": "Der Rest des Jahres" + }, + "values": { + "now": "Jetzt", + "startOfToday": "Beginn des heutigen Tages", + "endOfToday": "Ende des heutigen Tages", + "beginningOflastWeek": "Beginn der letzten Woche", + "endOfLastWeek": "Ende der letzten Woche", + "beginningOfThisWeek": "Beginn dieser Woche", + "endOfThisWeek": "Ende dieser Woche", + "startOfNextWeek": "Beginn der nächsten Woche", + "endOfNextWeek": "Ende der nächsten Woche", + "in7Days": "In 7 Tagen", + "beginningOfLastMonth": "Beginn des letzten Monats", + "endOfLastMonth": "Ende des letzten Monats", + "startOfThisMonth": "Beginn diesen Monats", + "endOfThisMonth": "Ende diesen Monats", + "startOfNextMonth": "Beginn des nächsten Monats", + "endOfNextMonth": "Ende des nächsten Monats", + "in30Days": "In 30 Tagen", + "startOfThisYear": "Beginn dieses Jahres", + "endOfThisYear": "Ende dieses Jahres" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Beschriibig", "descriptionPlaceholder": "Gib eine Beschreibung für dieses Team ein, drücke '/' für mehr Optionen…", "admin": "Chef", - "member": "Mitglied" + "member": "Mitglied", + "isPublic": "Öffentliches Team", + "isPublicDescription": "Machs das Team öffentlich sichtbar. Wenn aktiviert, kann jede:r mit diesem Team Projekte teilen, auch wenn er oder sie kein direktes Mitglied des Teams ist." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "j M Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Fähler", "success": "Erfolg", diff --git a/frontend/src/i18n/lang/eo-UY.json b/frontend/src/i18n/lang/eo-UY.json index 3c444813e..1ba512373 100644 --- a/frontend/src/i18n/lang/eo-UY.json +++ b/frontend/src/i18n/lang/eo-UY.json @@ -248,6 +248,7 @@ "text2": "This includes all tasks and CANNOT BE UNDONE!", "success": "The project was successfully deleted.", "tasksToDelete": "This will irrevocably remove approx. {count} tasks.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "This project does not contain any tasks, it should be safe to delete." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filters", "clear": "Clear Filters", + "showResults": "Show results", "attributes": { "title": "Title", "titlePlaceholder": "The saved filter title goes here…", @@ -415,6 +417,52 @@ "edit": { "title": "Edit This Saved Filter", "success": "The filter was saved successfully." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "To", "from": "From", "fromto": "{from} to {to}", + "date": "Date", "ranges": { "today": "Today", "thisWeek": "This Week", @@ -598,6 +647,27 @@ "lastMonth": "Last Month", "thisYear": "This Year", "restOfThisYear": "The Rest of This Year" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Description", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "Admin", - "member": "Member" + "member": "Member", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "j M Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Error", "success": "Success", diff --git a/frontend/src/i18n/lang/es-ES.json b/frontend/src/i18n/lang/es-ES.json index 16536a433..85ce3246c 100644 --- a/frontend/src/i18n/lang/es-ES.json +++ b/frontend/src/i18n/lang/es-ES.json @@ -248,6 +248,7 @@ "text2": "¡Esto incluye todas las tareas y NO SE PUEDE DESHACER!", "success": "El proyecto se eliminó con éxito.", "tasksToDelete": "Esto eliminará de forma definitiva aprox. {count} tareas.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "Este proyecto no contiene tareas. Debería ser seguro eliminarlo." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filtros", "clear": "Limpiar Filtros", + "showResults": "Show results", "attributes": { "title": "Título", "titlePlaceholder": "El título del filtro guardado va aquí…", @@ -415,6 +417,52 @@ "edit": { "title": "Editar este Filtro Guardado", "success": "El filtro se guardó con éxito." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "Para", "from": "De", "fromto": "De {from} para {to}", + "date": "Date", "ranges": { "today": "Hoy", "thisWeek": "Esta Semana", @@ -598,6 +647,27 @@ "lastMonth": "El Mes Pasado", "thisYear": "Este Año", "restOfThisYear": "El Resto del Año" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Descripción", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "Admin", - "member": "Miembro" + "member": "Miembro", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "j M Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Error", "success": "Éxito", diff --git a/frontend/src/i18n/lang/fr-FR.json b/frontend/src/i18n/lang/fr-FR.json index 5e9d6fc91..ed12f9d9c 100644 --- a/frontend/src/i18n/lang/fr-FR.json +++ b/frontend/src/i18n/lang/fr-FR.json @@ -248,6 +248,7 @@ "text2": "Ceci inclut toutes les tâches et NE PEUT PAS ÊTRE ANNULÉ !", "success": "Le projet a bien été supprimé.", "tasksToDelete": "Cela supprimera définitivement environ {count} tâches.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "Ce projet ne contient aucune tâche, vous pouvez le supprimer sans problème." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filtres", "clear": "Effacer les filtres", + "showResults": "Show results", "attributes": { "title": "Nom", "titlePlaceholder": "Entre un nom de filtre enregistré…", @@ -415,6 +417,52 @@ "edit": { "title": "Modifier ce filtre enregistré", "success": "Filtre enregistré." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "À", "from": "De", "fromto": "Du {from} au {to}", + "date": "Date", "ranges": { "today": "Aujourd’hui", "thisWeek": "Cette semaine", @@ -598,6 +647,27 @@ "lastMonth": "Le mois dernier", "thisYear": "Cette année", "restOfThisYear": "Le reste de cette année" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Description", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "Admin", - "member": "Membre" + "member": "Membre", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "j M Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Erreur", "success": "Succès", diff --git a/frontend/src/i18n/lang/hu-HU.json b/frontend/src/i18n/lang/hu-HU.json index 3163bec38..fa5aca3e0 100644 --- a/frontend/src/i18n/lang/hu-HU.json +++ b/frontend/src/i18n/lang/hu-HU.json @@ -248,6 +248,7 @@ "text2": "Ez magában foglalja az összes feladatot és NEM VISSZAVONHATÓ!", "success": "A projekt sikeresen törölve.", "tasksToDelete": "Ezzel visszavonhatatlanul eltávolítjuk kb. {count} feladatát.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "Ez a projekt nem tartalmaz feladatokat, biztonságosan törölhető." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Szűrők", "clear": "Szűrők törlése", + "showResults": "Show results", "attributes": { "title": "Cím", "titlePlaceholder": "A mentett szűrő címe ide kerül…", @@ -415,6 +417,52 @@ "edit": { "title": "Mentett szűrő szerkesztése", "success": "A szűrőt sikeresen mentette." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "Eddig", "from": "Ettől", "fromto": "{from} - tól {to} - ig", + "date": "Date", "ranges": { "today": "Ma", "thisWeek": "Ezen a héten", @@ -598,6 +647,27 @@ "lastMonth": "Előző hónap", "thisYear": "Aktuális év", "restOfThisYear": "Az év hátralévő része" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Leírás", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "Adminisztrátor", - "member": "Tag" + "member": "Tag", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "j M Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Hiba", "success": "Siker", diff --git a/frontend/src/i18n/lang/it-IT.json b/frontend/src/i18n/lang/it-IT.json index 1384b3a8a..934b5e61d 100644 --- a/frontend/src/i18n/lang/it-IT.json +++ b/frontend/src/i18n/lang/it-IT.json @@ -248,6 +248,7 @@ "text2": "This includes all tasks and CANNOT BE UNDONE!", "success": "The project was successfully deleted.", "tasksToDelete": "This will irrevocably remove approx. {count} tasks.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "This project does not contain any tasks, it should be safe to delete." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filtri", "clear": "Pulisci Filtri", + "showResults": "Show results", "attributes": { "title": "Titolo", "titlePlaceholder": "Il titolo del filtro salvato va qui…", @@ -415,6 +417,52 @@ "edit": { "title": "Modifica Questo Filtro Salvato", "success": "Filtro salvato." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "A", "from": "Da", "fromto": "da {from} a {to}", + "date": "Date", "ranges": { "today": "Oggi", "thisWeek": "Questa Settimana", @@ -598,6 +647,27 @@ "lastMonth": "Mese scorso", "thisYear": "Quest'anno", "restOfThisYear": "Il resto di quest'anno" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Descrizione", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "Amministratore", - "member": "Membro" + "member": "Membro", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "j M Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Errore", "success": "Fatto", diff --git a/frontend/src/i18n/lang/ja-JP.json b/frontend/src/i18n/lang/ja-JP.json index ec3cb11e7..5050264b0 100644 --- a/frontend/src/i18n/lang/ja-JP.json +++ b/frontend/src/i18n/lang/ja-JP.json @@ -248,6 +248,7 @@ "text2": "このプロジェクトに含まれるタスクはすべて削除されます。この操作は元に戻せません。", "success": "プロジェクトは正常に削除されました。", "tasksToDelete": "約{count}件のタスクが抹消されます。", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "このプロジェクトにはタスクが含まれていないので問題なく削除できます。" }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "絞り込み", "clear": "絞り込みの解除", + "showResults": "Show results", "attributes": { "title": "絞り込み条件名", "titlePlaceholder": "絞り込み条件名を入力…", @@ -415,6 +417,52 @@ "edit": { "title": "絞り込み条件の編集", "success": "絞り込み条件は正常に保存されました。" + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "タスクの終了日", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "終了", "from": "開始", "fromto": "{from} 〜 {to} まで", + "date": "Date", "ranges": { "today": "今日", "thisWeek": "今週", @@ -598,6 +647,27 @@ "lastMonth": "先月", "thisYear": "今年", "restOfThisYear": "今から年末まで" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "今日の終わり", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "先週の終わり", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "今週の終わり", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "来週の終わり", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "先月の終わり", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "今月の終わり", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "来月の終わり", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "今年の終わり" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "説明", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "管理者", - "member": "メンバー" + "member": "メンバー", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "Y/n/j H:i", "altFormatShort": "Y/n/j" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Error", "success": "Success", diff --git a/frontend/src/i18n/lang/ko-KR.json b/frontend/src/i18n/lang/ko-KR.json index 9d317edc9..2ccb6833b 100644 --- a/frontend/src/i18n/lang/ko-KR.json +++ b/frontend/src/i18n/lang/ko-KR.json @@ -248,6 +248,7 @@ "text2": "This includes all tasks and CANNOT BE UNDONE!", "success": "프로젝트가 성공적으로 삭제되었습니다.", "tasksToDelete": "This will irrevocably remove approx. {count} tasks.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "This project does not contain any tasks, it should be safe to delete." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "필터", "clear": "필터 초기화", + "showResults": "Show results", "attributes": { "title": "제목", "titlePlaceholder": "The saved filter title goes here…", @@ -415,6 +417,52 @@ "edit": { "title": "Edit This Saved Filter", "success": "The filter was saved successfully." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "To", "from": "From", "fromto": "{from} 에서 {to}", + "date": "Date", "ranges": { "today": "오늘", "thisWeek": "이번 주", @@ -598,6 +647,27 @@ "lastMonth": "지난달", "thisYear": "올해", "restOfThisYear": "The Rest of This Year" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Description", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "Admin", - "member": "Member" + "member": "Member", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "j M Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Error", "success": "Success", diff --git a/frontend/src/i18n/lang/nl-NL.json b/frontend/src/i18n/lang/nl-NL.json index a376841ca..d5824f257 100644 --- a/frontend/src/i18n/lang/nl-NL.json +++ b/frontend/src/i18n/lang/nl-NL.json @@ -248,6 +248,7 @@ "text2": "This includes all tasks and CANNOT BE UNDONE!", "success": "The project was successfully deleted.", "tasksToDelete": "This will irrevocably remove approx. {count} tasks.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "This project does not contain any tasks, it should be safe to delete." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filters", "clear": "Clear Filters", + "showResults": "Show results", "attributes": { "title": "Titel", "titlePlaceholder": "The saved filter title goes here…", @@ -415,6 +417,52 @@ "edit": { "title": "Edit This Saved Filter", "success": "Het filter is succesvol opgeslagen." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "To", "from": "From", "fromto": "{from} to {to}", + "date": "Date", "ranges": { "today": "Today", "thisWeek": "This Week", @@ -598,6 +647,27 @@ "lastMonth": "Last Month", "thisYear": "This Year", "restOfThisYear": "The Rest of This Year" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Beschrijving", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "Beheerder", - "member": "Lid" + "member": "Lid", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { diff --git a/frontend/src/i18n/lang/no-NO.json b/frontend/src/i18n/lang/no-NO.json index 0acd59186..10b2f0568 100644 --- a/frontend/src/i18n/lang/no-NO.json +++ b/frontend/src/i18n/lang/no-NO.json @@ -248,6 +248,7 @@ "text2": "Dette inkluderer alle oppgaver og KAN IKKE ANGRES!", "success": "Prosjektet ble slettet.", "tasksToDelete": "Dette vil ugjenkallelig fjerne ca. {count} oppgaver.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "Dette prosjektet inneholder ingen oppgaver, det bør være trygt å slette." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filtre", "clear": "Fjern filtre", + "showResults": "Show results", "attributes": { "title": "Tittel", "titlePlaceholder": "Den lagrede filtertittelen kommer hit…", @@ -415,6 +417,52 @@ "edit": { "title": "Rediger dette lagrede filteret", "success": "Filteret ble lagret." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "Til", "from": "Fra", "fromto": "{from} til {to}", + "date": "Date", "ranges": { "today": "Idag", "thisWeek": "Denne uken", @@ -598,6 +647,27 @@ "lastMonth": "Forrige måned", "thisYear": "Dette året", "restOfThisYear": "Resten av denne uken" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Beskrivelse", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "Administrator", - "member": "Medlem" + "member": "Medlem", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "d.m.Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Feil", "success": "Suksess", diff --git a/frontend/src/i18n/lang/pl-PL.json b/frontend/src/i18n/lang/pl-PL.json index bfa4b752c..d164a0a31 100644 --- a/frontend/src/i18n/lang/pl-PL.json +++ b/frontend/src/i18n/lang/pl-PL.json @@ -248,6 +248,7 @@ "text2": "Dotyczy to wszystkich zadań i tego NIE DA SIĘ COFNĄĆ!", "success": "Projekt został pomyślnie usunięty.", "tasksToDelete": "To nieodwracalnie usunie około {count} zadań.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "Ten projekt nie zawiera żadnych zadań, więc można go bezpiecznie usunąć." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filtry", "clear": "Wyczyść filtry", + "showResults": "Show results", "attributes": { "title": "Tytuł", "titlePlaceholder": "Tu wpisz tytuł filtra stałego…", @@ -415,6 +417,52 @@ "edit": { "title": "Edytuj ten filtr stały", "success": "Filtr został pomyślnie zapisany." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "Do", "from": "Od", "fromto": "{from} do {to}", + "date": "Date", "ranges": { "today": "Dziś", "thisWeek": "W tym tygodniu", @@ -598,6 +647,27 @@ "lastMonth": "Zeszły miesiąc", "thisYear": "Ten rok", "restOfThisYear": "Reszta tego roku" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Opis", "descriptionPlaceholder": "Opisz tutaj zespół, naciśnij '/' aby uzyskać więcej opcji…", "admin": "Administrator", - "member": "Członek" + "member": "Członek", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "j M Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Błąd", "success": "Sukces", diff --git a/frontend/src/i18n/lang/pt-BR.json b/frontend/src/i18n/lang/pt-BR.json index 2c22590ac..61a4b9f06 100644 --- a/frontend/src/i18n/lang/pt-BR.json +++ b/frontend/src/i18n/lang/pt-BR.json @@ -248,6 +248,7 @@ "text2": "Isso inclui todas as tarefas e NÃO PODE SER DESFEITO!", "success": "Seu projeto foi excluído com sucesso.", "tasksToDelete": "Isto irá remover permanentemente approx. {count} tarefas.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "Este projeto não contém tarefas, é seguro excluir." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filtros", "clear": "Limpar Filtros", + "showResults": "Show results", "attributes": { "title": "Título", "titlePlaceholder": "O título do filtro salvo fica aqui…", @@ -415,6 +417,52 @@ "edit": { "title": "Editar este filtro salvo", "success": "O filtro foi salvo com sucesso." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "Para", "from": "De", "fromto": "{from} até {to}", + "date": "Date", "ranges": { "today": "Hoje", "thisWeek": "Esta semana", @@ -598,6 +647,27 @@ "lastMonth": "Último mês", "thisYear": "Este ano", "restOfThisYear": "O resto deste ano" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Descrição", "descriptionPlaceholder": "Descreva a equipe aqui, aperte '/' para mais opções…", "admin": "Administrador", - "member": "Membro" + "member": "Membro", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "j M Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Erro", "success": "Sucesso", diff --git a/frontend/src/i18n/lang/pt-PT.json b/frontend/src/i18n/lang/pt-PT.json index 0403b4c7b..57ab411cd 100644 --- a/frontend/src/i18n/lang/pt-PT.json +++ b/frontend/src/i18n/lang/pt-PT.json @@ -248,6 +248,7 @@ "text2": "Isto inclui todas as tarefas e NÃO PODE SER REVERTIDO!", "success": "O projeto foi eliminado com sucesso.", "tasksToDelete": "Isto irá remover irrevogavelmente aprox. {count} tarefas.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "Este projeto não contém tarefas, deve ser seguro eliminá-lo." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filtros", "clear": "Limpar Filtros", + "showResults": "Show results", "attributes": { "title": "Título", "titlePlaceholder": "O título do filtro memorizado será aqui…", @@ -415,6 +417,52 @@ "edit": { "title": "Editar Este Filtro Memorizado", "success": "O filtro foi memorizado com sucesso." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "Até", "from": "De", "fromto": "{from} até {to}", + "date": "Date", "ranges": { "today": "Hoje", "thisWeek": "Esta semana", @@ -598,6 +647,27 @@ "lastMonth": "Mês Passado", "thisYear": "Este Ano", "restOfThisYear": "O Resto Deste Ano" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Descrição", "descriptionPlaceholder": "Descreve aqui a equipa, pressiona '/' para mais opções…", "admin": "Administrador", - "member": "Membro" + "member": "Membro", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "j M Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Erro", "success": "Sucesso", diff --git a/frontend/src/i18n/lang/ro-RO.json b/frontend/src/i18n/lang/ro-RO.json index 3c444813e..1ba512373 100644 --- a/frontend/src/i18n/lang/ro-RO.json +++ b/frontend/src/i18n/lang/ro-RO.json @@ -248,6 +248,7 @@ "text2": "This includes all tasks and CANNOT BE UNDONE!", "success": "The project was successfully deleted.", "tasksToDelete": "This will irrevocably remove approx. {count} tasks.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "This project does not contain any tasks, it should be safe to delete." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filters", "clear": "Clear Filters", + "showResults": "Show results", "attributes": { "title": "Title", "titlePlaceholder": "The saved filter title goes here…", @@ -415,6 +417,52 @@ "edit": { "title": "Edit This Saved Filter", "success": "The filter was saved successfully." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "To", "from": "From", "fromto": "{from} to {to}", + "date": "Date", "ranges": { "today": "Today", "thisWeek": "This Week", @@ -598,6 +647,27 @@ "lastMonth": "Last Month", "thisYear": "This Year", "restOfThisYear": "The Rest of This Year" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Description", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "Admin", - "member": "Member" + "member": "Member", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "j M Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Error", "success": "Success", diff --git a/frontend/src/i18n/lang/ru-RU.json b/frontend/src/i18n/lang/ru-RU.json index c2186a5cf..7ac95bdad 100644 --- a/frontend/src/i18n/lang/ru-RU.json +++ b/frontend/src/i18n/lang/ru-RU.json @@ -248,6 +248,7 @@ "text2": "Это включает в себя все задачи, и отменить это будет нельзя!", "success": "Проект успешно удалён.", "tasksToDelete": "Это безвозвратно удалит примерно {count} задач.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "В этом проекте нет никаких задач, можно спокойно удалять." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Фильтры", "clear": "Сбросить фильтры", + "showResults": "Show results", "attributes": { "title": "Название", "titlePlaceholder": "Введите название сохранённого фильтра…", @@ -415,6 +417,52 @@ "edit": { "title": "Изменить этот сохранённый фильтр", "success": "Фильтр сохранён." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "По", "from": "С", "fromto": "С {from} по {to}", + "date": "Date", "ranges": { "today": "Сегодня", "thisWeek": "Эта неделя", @@ -598,6 +647,27 @@ "lastMonth": "Прошлый месяц", "thisYear": "Этот год", "restOfThisYear": "Остаток этого года" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Описание", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "Администратор", - "member": "Участник" + "member": "Участник", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "j M Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Ошибка", "success": "Успех", diff --git a/frontend/src/i18n/lang/sk-SK.json b/frontend/src/i18n/lang/sk-SK.json index 3c444813e..1ba512373 100644 --- a/frontend/src/i18n/lang/sk-SK.json +++ b/frontend/src/i18n/lang/sk-SK.json @@ -248,6 +248,7 @@ "text2": "This includes all tasks and CANNOT BE UNDONE!", "success": "The project was successfully deleted.", "tasksToDelete": "This will irrevocably remove approx. {count} tasks.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "This project does not contain any tasks, it should be safe to delete." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filters", "clear": "Clear Filters", + "showResults": "Show results", "attributes": { "title": "Title", "titlePlaceholder": "The saved filter title goes here…", @@ -415,6 +417,52 @@ "edit": { "title": "Edit This Saved Filter", "success": "The filter was saved successfully." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "To", "from": "From", "fromto": "{from} to {to}", + "date": "Date", "ranges": { "today": "Today", "thisWeek": "This Week", @@ -598,6 +647,27 @@ "lastMonth": "Last Month", "thisYear": "This Year", "restOfThisYear": "The Rest of This Year" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Description", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "Admin", - "member": "Member" + "member": "Member", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "j M Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Error", "success": "Success", diff --git a/frontend/src/i18n/lang/sl-SI.json b/frontend/src/i18n/lang/sl-SI.json index e162c0a26..ed4073644 100644 --- a/frontend/src/i18n/lang/sl-SI.json +++ b/frontend/src/i18n/lang/sl-SI.json @@ -57,11 +57,11 @@ "logout": "Odjava", "emailInvalid": "Prosim vnesite veljaven e-poštni naslov.", "usernameRequired": "Prosim vnesite uporabniško ime.", - "usernameMustNotContainSpace": "The username must not contain spaces.", - "usernameMustNotLookLikeUrl": "The username must not look like a URL.", + "usernameMustNotContainSpace": "Uporabniško ime ne sme vsebovati presledkov.", + "usernameMustNotLookLikeUrl": "Uporabniško ime ne sme izgledati kot URL.", "passwordRequired": "Prosim vnesite geslo.", - "passwordNotMin": "Password must have at least 8 characters.", - "passwordNotMax": "Password must have at most 250 characters.", + "passwordNotMin": "Geslo mora imeti vsaj 8 znakov.", + "passwordNotMax": "Geslo mora imeti največ 250 znakov.", "showPassword": "Prikažite geslo", "hidePassword": "Skrijte geslo", "noAccountYet": "Še nimate računa?", @@ -248,6 +248,7 @@ "text2": "To vključuje vse naloge in GA NI MOGOČE RAZVELJAVITI!", "success": "Projekt je bil uspešno izbrisan.", "tasksToDelete": "S tem bo nepreklicno odstranjeno približno {count} nalog.", + "tasksAndChildProjectsToDelete": "S tem bo nepreklicno odstranjeno cca. {tasks} nalog in {projects} projektov.", "noTasksToDelete": "Ta projekt ne vsebuje nobenih nalog, zato ga lahko varno izbrišete." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filtri", "clear": "Počisti filtre", + "showResults": "Prikaži rezultate", "attributes": { "title": "Naslov", "titlePlaceholder": "Tu je naslov shranjenega filtra…", @@ -415,6 +417,52 @@ "edit": { "title": "Uredi shranjeni filter", "success": "Filter je bil uspešno shranjen." + }, + "query": { + "title": "Poizvedba", + "placeholder": "Vnesite poizvedbo za iskanje ali filtriranje…", + "help": { + "intro": "Za filtriranje nalog lahko uporabite sintakso poizvedbe, podobno SQL. Razpoložljiva polja za filtriranje vključujejo:", + "link": "Kako to deluje?", + "canUseDatemath": "Za nastavitev relativnih datumov lahko določite datume. Če želite izvedeti več, kliknite vrednost datuma v poizvedbi.", + "fields": { + "done": "Ali je naloga opravljena ali ne", + "priority": "Stopnja prioritete naloge (1-5)", + "percentDone": "Odstotek dokončanja naloge (0–100)", + "dueDate": "Datum zapadlosti naloge", + "startDate": "Začetni datum opravila", + "endDate": "Končni datum opravila", + "doneAt": "Datum in čas, ko je bila naloga opravljena", + "assignees": "Dodeljeni k nalogi", + "labels": "Oznake, povezane z opravilom", + "project": "Projekt, ki mu naloga pripada (na voljo samo za shranjene filtre, ne na ravni projekta)" + }, + "operators": { + "intro": "Razpoložljivi operaterji za filtriranje vključujejo:", + "notEqual": "Ni enako", + "equal": "Je enako", + "greaterThan": "Večje kot", + "greaterThanOrEqual": "Večje ali enako kot", + "lessThan": "Manj kot", + "lessThanOrEqual": "Manjše ali enako kot", + "like": "Ujema se z vzorcem (z nadomestnimi znaki %)", + "in": "Ujema se s katero koli vrednostjo na seznamu vrednosti, ločenih z vejico" + }, + "logicalOperators": { + "intro": "Če želite združiti več pogojev, lahko uporabite naslednje logične operatorje:", + "and": "IN operator, se ujema, če so izpolnjeni vsi pogoji", + "or": "ALI operator, se ujema, če je kateri od pogojev resničen", + "parentheses": "Oklepaji za združevanje pogojev" + }, + "examples": { + "intro": "Tukaj je nekaj primerov filtrirnih poizvedb:", + "priorityEqual": "Ujema se z nalogami s prioritetno stopnjo 4", + "dueDatePast": "Ujema se z opravili z rokom v preteklosti", + "undoneHighPriority": "Ujema se z neopravljenimi opravili s prioritetno stopnjo 3 ali višjo", + "assigneesIn": "Ujema se z nalogami, dodeljenimi k \"user1\" ali \"user2\"", + "priorityOneOrTwoPastDue": "Ujema se z opravili s prioritetno stopnjo 1 ali 2 in rokom v preteklosti" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "Za", "from": "Od", "fromto": "{from} do {to}", + "date": "Datum", "ranges": { "today": "Danes", "thisWeek": "Ta teden", @@ -598,6 +647,27 @@ "lastMonth": "Prejšnji mesec", "thisYear": "Letos", "restOfThisYear": "Preostanek tega leta" + }, + "values": { + "now": "Zdaj", + "startOfToday": "Začetek današnjega dne", + "endOfToday": "Konec današnjega dne", + "beginningOflastWeek": "Začetek prejšnjega tedna", + "endOfLastWeek": "Konec prejšnjega tedna", + "beginningOfThisWeek": "Začetek tega tedna", + "endOfThisWeek": "Konec tega tedna", + "startOfNextWeek": "Začetek naslednjega tedna", + "endOfNextWeek": "Konec naslednjega tedna", + "in7Days": "V 7 dneh", + "beginningOfLastMonth": "Začetek prejšnjega meseca", + "endOfLastMonth": "Konec prejšnjega meseca", + "startOfThisMonth": "Začetek tega meseca", + "endOfThisMonth": "Konec tega meseca", + "startOfNextMonth": "Začetek naslednjega meseca", + "endOfNextMonth": "Konec naslednjega meseca", + "in30Days": "V 30 dneh", + "startOfThisYear": "Začetek tega leta", + "endOfThisYear": "Konec tega leta" } }, "datemathHelp": { @@ -713,7 +783,7 @@ "startDate": "Začetni datum", "title": "Naslov", "updated": "Posodobljeno", - "doneAt": "Done At" + "doneAt": "Končano ob" }, "subscription": { "subscribedTaskThroughParentProject": "Ker ste na to nalogo naročeni prek njenega projekta, se tu ne morete odjaviti.", @@ -916,7 +986,9 @@ "description": "Opis", "descriptionPlaceholder": "Tukaj opiši ekipo, pritisni '/' za več možnosti…", "admin": "Administrator", - "member": "Član" + "member": "Član", + "isPublic": "Javna ekipa", + "isPublicDescription": "Naj bo ekipa javno vidna. Ko je omogočeno, lahko vsakdo deli projekte s to ekipo, tudi če ni neposredni član." } }, "keyboardShortcuts": { @@ -975,8 +1047,8 @@ "share": "Skupna raba", "newProject": "Nov projekt", "createProject": "Ustvari projekt", - "cantArchiveIsDefault": "You cannot archive this because it is your default project.", - "cantDeleteIsDefault": "You cannot delete this because it is your default project." + "cantArchiveIsDefault": "Tega ne morete arhivirati, ker je to vaš privzeti projekt.", + "cantDeleteIsDefault": "Tega ne morete izbrisati, ker je to vaš privzeti projekt." }, "apiConfig": { "url": "Vikunja URL", @@ -1023,6 +1095,12 @@ "altFormatLong": "j M Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Napaka", "success": "Uspeh", @@ -1096,7 +1174,7 @@ }, "about": { "title": "O programu", - "version": "Version: {version}" + "version": "Verzija: {version}" }, "time": { "units": { diff --git a/frontend/src/i18n/lang/sr-CS.json b/frontend/src/i18n/lang/sr-CS.json index 3c444813e..1ba512373 100644 --- a/frontend/src/i18n/lang/sr-CS.json +++ b/frontend/src/i18n/lang/sr-CS.json @@ -248,6 +248,7 @@ "text2": "This includes all tasks and CANNOT BE UNDONE!", "success": "The project was successfully deleted.", "tasksToDelete": "This will irrevocably remove approx. {count} tasks.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "This project does not contain any tasks, it should be safe to delete." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filters", "clear": "Clear Filters", + "showResults": "Show results", "attributes": { "title": "Title", "titlePlaceholder": "The saved filter title goes here…", @@ -415,6 +417,52 @@ "edit": { "title": "Edit This Saved Filter", "success": "The filter was saved successfully." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "To", "from": "From", "fromto": "{from} to {to}", + "date": "Date", "ranges": { "today": "Today", "thisWeek": "This Week", @@ -598,6 +647,27 @@ "lastMonth": "Last Month", "thisYear": "This Year", "restOfThisYear": "The Rest of This Year" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Description", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "Admin", - "member": "Member" + "member": "Member", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "j M Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Error", "success": "Success", diff --git a/frontend/src/i18n/lang/sv-SE.json b/frontend/src/i18n/lang/sv-SE.json index d4540142a..c41c219fe 100644 --- a/frontend/src/i18n/lang/sv-SE.json +++ b/frontend/src/i18n/lang/sv-SE.json @@ -248,6 +248,7 @@ "text2": "This includes all tasks and CANNOT BE UNDONE!", "success": "The project was successfully deleted.", "tasksToDelete": "This will irrevocably remove approx. {count} tasks.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "This project does not contain any tasks, it should be safe to delete." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filter", "clear": "Rensa filter", + "showResults": "Visa resultat", "attributes": { "title": "Titel", "titlePlaceholder": "The saved filter title goes here…", @@ -415,6 +417,52 @@ "edit": { "title": "Edit This Saved Filter", "success": "The filter was saved successfully." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Lika med", + "greaterThan": "Större än", + "greaterThanOrEqual": "Större än eller lika med", + "lessThan": "Mindre än", + "lessThanOrEqual": "Mindre än eller lika med", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "Till", "from": "Från", "fromto": "{from} till {to}", + "date": "Datum", "ranges": { "today": "I dag", "thisWeek": "Denna vecka", @@ -598,6 +647,27 @@ "lastMonth": "Förra månaden", "thisYear": "This Year", "restOfThisYear": "The Rest of This Year" + }, + "values": { + "now": "Nu", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Beskrivning", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "Admin", - "member": "Medlem" + "member": "Medlem", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "j M Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Error", "success": "Success", diff --git a/frontend/src/i18n/lang/tr-TR.json b/frontend/src/i18n/lang/tr-TR.json index 3c444813e..1ba512373 100644 --- a/frontend/src/i18n/lang/tr-TR.json +++ b/frontend/src/i18n/lang/tr-TR.json @@ -248,6 +248,7 @@ "text2": "This includes all tasks and CANNOT BE UNDONE!", "success": "The project was successfully deleted.", "tasksToDelete": "This will irrevocably remove approx. {count} tasks.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "This project does not contain any tasks, it should be safe to delete." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filters", "clear": "Clear Filters", + "showResults": "Show results", "attributes": { "title": "Title", "titlePlaceholder": "The saved filter title goes here…", @@ -415,6 +417,52 @@ "edit": { "title": "Edit This Saved Filter", "success": "The filter was saved successfully." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "To", "from": "From", "fromto": "{from} to {to}", + "date": "Date", "ranges": { "today": "Today", "thisWeek": "This Week", @@ -598,6 +647,27 @@ "lastMonth": "Last Month", "thisYear": "This Year", "restOfThisYear": "The Rest of This Year" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Description", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "Admin", - "member": "Member" + "member": "Member", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "j M Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Error", "success": "Success", diff --git a/frontend/src/i18n/lang/vi-VN.json b/frontend/src/i18n/lang/vi-VN.json index 75ac1ce16..b34d94f96 100644 --- a/frontend/src/i18n/lang/vi-VN.json +++ b/frontend/src/i18n/lang/vi-VN.json @@ -248,6 +248,7 @@ "text2": "This includes all tasks and CANNOT BE UNDONE!", "success": "The project was successfully deleted.", "tasksToDelete": "This will irrevocably remove approx. {count} tasks.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "This project does not contain any tasks, it should be safe to delete." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Bộ lọc", "clear": "Xoá các bộ lọc", + "showResults": "Show results", "attributes": { "title": "Tiêu đề", "titlePlaceholder": "Tiêu đề bộ lọc đã lưu ở đây…", @@ -415,6 +417,52 @@ "edit": { "title": "Sửa bộ lọc sẵn này", "success": "Bộ lọc đã được lưu thành công." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "Đến", "from": "Từ", "fromto": "{from} đến {to}", + "date": "Date", "ranges": { "today": "Hôm nay", "thisWeek": "Tuần này", @@ -598,6 +647,27 @@ "lastMonth": "Tháng trước", "thisYear": "Năm nay", "restOfThisYear": "Toàn bộ ngày còn lại trong năm" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Mô tả", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "Quản trị viên", - "member": "Thành viên" + "member": "Thành viên", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { @@ -1023,6 +1095,12 @@ "altFormatLong": "j M Y H:i", "altFormatShort": "j M Y" }, + "reaction": { + "reactedWith": "{user} reacted with {value}", + "reactedWithAnd": "{users} and {lastUser} reacted with {value}", + "reactedWithAndMany": "{users} and {num} more reacted reacted with {value}", + "add": "Add your reaction" + }, "error": { "error": "Lỗi", "success": "Thành công", diff --git a/frontend/src/i18n/lang/zh-CN.json b/frontend/src/i18n/lang/zh-CN.json index d0bd62c64..6cd78c06e 100644 --- a/frontend/src/i18n/lang/zh-CN.json +++ b/frontend/src/i18n/lang/zh-CN.json @@ -248,6 +248,7 @@ "text2": "这包括所有的任务,并且无法撤销!", "success": "项目已成功删除。", "tasksToDelete": "此操作无法撤消!将移除大约 {count} 个任务。", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "此列表不包含任何任务,可以安全删除。" }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "筛选器", "clear": "清除筛选条件", + "showResults": "Show results", "attributes": { "title": "标题", "titlePlaceholder": "填写筛选器标题", @@ -415,6 +417,52 @@ "edit": { "title": "编辑此保存的过滤器", "success": "过滤器保存成功。" + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "到", "from": "开始", "fromto": "{from} 到 {to}", + "date": "Date", "ranges": { "today": "今天", "thisWeek": "本周", @@ -598,6 +647,27 @@ "lastMonth": "上个月", "thisYear": "今年", "restOfThisYear": "本年度剩余时间" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "描述信息", "descriptionPlaceholder": "在此描述团队,点击'/'获取更多选项…", "admin": "管理员", - "member": "成员" + "member": "成员", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { diff --git a/frontend/src/i18n/lang/zh-TW.json b/frontend/src/i18n/lang/zh-TW.json index 3c444813e..2028930a3 100644 --- a/frontend/src/i18n/lang/zh-TW.json +++ b/frontend/src/i18n/lang/zh-TW.json @@ -248,6 +248,7 @@ "text2": "This includes all tasks and CANNOT BE UNDONE!", "success": "The project was successfully deleted.", "tasksToDelete": "This will irrevocably remove approx. {count} tasks.", + "tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.", "noTasksToDelete": "This project does not contain any tasks, it should be safe to delete." }, "duplicate": { @@ -385,6 +386,7 @@ "filters": { "title": "Filters", "clear": "Clear Filters", + "showResults": "Show results", "attributes": { "title": "Title", "titlePlaceholder": "The saved filter title goes here…", @@ -415,6 +417,52 @@ "edit": { "title": "Edit This Saved Filter", "success": "The filter was saved successfully." + }, + "query": { + "title": "Query", + "placeholder": "Type a search or filter query…", + "help": { + "intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:", + "link": "How does this work?", + "canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.", + "fields": { + "done": "Whether the task is completed or not", + "priority": "The priority level of the task (1-5)", + "percentDone": "The percentage of completion for the task (0-100)", + "dueDate": "The due date of the task", + "startDate": "The start date of the task", + "endDate": "The end date of the task", + "doneAt": "The date and time when the task was completed", + "assignees": "The assignees of the task", + "labels": "The labels associated with the task", + "project": "The project the task belongs to (only available for saved filters, not on a project level)" + }, + "operators": { + "intro": "The available operators for filtering include:", + "notEqual": "Not equal to", + "equal": "Equal to", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal to", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal to", + "like": "Matches a pattern (using wildcard %)", + "in": "Matches any value in a comma-seperated list of values" + }, + "logicalOperators": { + "intro": "To combine multiple conditions, you can use the following logical operators:", + "and": "AND operator, matches if all conditions are true", + "or": "OR operator, matches if any of the conditions are true", + "parentheses": "Parentheses for grouping conditions" + }, + "examples": { + "intro": "Here are some examples of filter queries:", + "priorityEqual": "Matches tasks with priority level 4", + "dueDatePast": "Matches tasks with a due date in the past", + "undoneHighPriority": "Matches undone tasks with priority level 3 or higher", + "assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"", + "priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past" + } + } } }, "migrate": { @@ -584,6 +632,7 @@ "to": "To", "from": "From", "fromto": "{from} to {to}", + "date": "Date", "ranges": { "today": "Today", "thisWeek": "This Week", @@ -598,6 +647,27 @@ "lastMonth": "Last Month", "thisYear": "This Year", "restOfThisYear": "The Rest of This Year" + }, + "values": { + "now": "Now", + "startOfToday": "Start of today", + "endOfToday": "End of today", + "beginningOflastWeek": "Beginning of last week", + "endOfLastWeek": "End of last week", + "beginningOfThisWeek": "Beginning of this week", + "endOfThisWeek": "End of this week", + "startOfNextWeek": "Start of next week", + "endOfNextWeek": "End of next week", + "in7Days": "In 7 days", + "beginningOfLastMonth": "Beginning of last month", + "endOfLastMonth": "End of last month", + "startOfThisMonth": "Start of this month", + "endOfThisMonth": "End of this month", + "startOfNextMonth": "Start of next month", + "endOfNextMonth": "End of next month", + "in30Days": "In 30 days", + "startOfThisYear": "Beginning of this year", + "endOfThisYear": "End of this year" } }, "datemathHelp": { @@ -916,7 +986,9 @@ "description": "Description", "descriptionPlaceholder": "Describe the team here, hit '/' for more options…", "admin": "Admin", - "member": "Member" + "member": "Member", + "isPublic": "Public Team", + "isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member." } }, "keyboardShortcuts": { From 17e222edfde7f690de824c5bb4f5232f9980f96b Mon Sep 17 00:00:00 2001 From: renovate Date: Wed, 13 Mar 2024 00:06:58 +0000 Subject: [PATCH 34/66] chore(deps): update dependency happy-dom to v13.8.2 --- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index ddf6a1aa2..4dc40c491 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -161,7 +161,7 @@ "esbuild": "0.20.1", "eslint": "8.57.0", "eslint-plugin-vue": "9.23.0", - "happy-dom": "13.7.8", + "happy-dom": "13.8.2", "histoire": "0.17.9", "postcss": "8.4.35", "postcss-easing-gradients": "3.0.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 02b83faa3..fe085dd35 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -340,8 +340,8 @@ devDependencies: specifier: 9.23.0 version: 9.23.0(eslint@8.57.0) happy-dom: - specifier: 13.7.8 - version: 13.7.8 + specifier: 13.8.2 + version: 13.8.2 histoire: specifier: 0.17.9 version: 0.17.9(@types/node@20.11.26)(sass@1.71.1)(terser@5.24.0)(vite@5.1.6) @@ -392,7 +392,7 @@ devDependencies: version: 5.1.0(vue@3.4.21) vitest: specifier: 1.3.1 - version: 1.3.1(@types/node@20.11.26)(happy-dom@13.7.8)(sass@1.71.1)(terser@5.24.0) + version: 1.3.1(@types/node@20.11.26)(happy-dom@13.8.2)(sass@1.71.1)(terser@5.24.0) vue-tsc: specifier: 2.0.6 version: 2.0.6(typescript@5.4.2) @@ -6460,8 +6460,8 @@ packages: strip-bom-string: 1.0.0 dev: true - /happy-dom@13.7.8: - resolution: {integrity: sha512-dnvgCiPPfXXts+AW1DVAoDa9nPmI48YPHUv34L6pmjv2lwNZte8OwsK9SajEXENfibS8uo1zG7xJwlW/NXlDxQ==} + /happy-dom@13.8.2: + resolution: {integrity: sha512-u9KxyeQNIzkJDR2iCitKeS5Uy0YUv5eOntpO8e7ZzbDVv4kP5Y77Zo2LnZitwMrss/1pY2Uc2e5qOVGkiKE5Gg==} engines: {node: '>=16.0.0'} dependencies: entities: 4.5.0 @@ -10090,7 +10090,7 @@ packages: fsevents: 2.3.3 dev: true - /vitest@1.3.1(@types/node@20.11.26)(happy-dom@13.7.8)(sass@1.71.1)(terser@5.24.0): + /vitest@1.3.1(@types/node@20.11.26)(happy-dom@13.8.2)(sass@1.71.1)(terser@5.24.0): resolution: {integrity: sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -10125,7 +10125,7 @@ packages: chai: 4.3.10 debug: 4.3.4(supports-color@8.1.1) execa: 8.0.1 - happy-dom: 13.7.8 + happy-dom: 13.8.2 local-pkg: 0.5.0 magic-string: 0.30.7 pathe: 1.1.1 From 99c55241156095a81f1a01bddfd7a6897abac894 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 13 Mar 2024 16:59:57 +0100 Subject: [PATCH 35/66] fix(editor): don't allow image upload when it's not possible to do it --- .../components/input/editor/EditorToolbar.vue | 8 ++--- .../src/components/input/editor/TipTap.vue | 29 ++++++++++++++----- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/input/editor/EditorToolbar.vue b/frontend/src/components/input/editor/EditorToolbar.vue index c31bcfe35..9018f79dd 100644 --- a/frontend/src/components/input/editor/EditorToolbar.vue +++ b/frontend/src/components/input/editor/EditorToolbar.vue @@ -139,7 +139,7 @@ @@ -347,16 +347,14 @@ const { editor: Editor, }>() +const emit = defineEmits(['imageUploadClicked']) + const tableMode = ref(false) function toggleTableMode() { tableMode.value = !tableMode.value } -function openImagePicker() { - document.getElementById('tiptap__image-upload').click() -} - function setLink(event) { setLinkInEditor(event.target.getBoundingClientRect(), editor) } diff --git a/frontend/src/components/input/editor/TipTap.vue b/frontend/src/components/input/editor/TipTap.vue index 1cc3c165d..0213546f6 100644 --- a/frontend/src/components/input/editor/TipTap.vue +++ b/frontend/src/components/input/editor/TipTap.vue @@ -6,7 +6,7 @@ { await nextTick() - const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0] - input?.addEventListener('paste', handleImagePaste) + if (typeof uploadCallback !== 'undefined') { + const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0] + input?.addEventListener('paste', handleImagePaste) + } setModeAndValue(modelValue) }) onBeforeUnmount(() => { nextTick(() => { - const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0] - input?.removeEventListener('paste', handleImagePaste) + if (typeof uploadCallback !== 'undefined') { + const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0] + input?.removeEventListener('paste', handleImagePaste) + } }) if (editShortcut !== '') { document.removeEventListener('keydown', setFocusToEditor) @@ -558,10 +571,10 @@ function handleImagePaste(event) { // See https://github.com/github/hotkey/discussions/85#discussioncomment-5214660 function setFocusToEditor(event) { - if(event.target.shadowRoot) { + if (event.target.shadowRoot) { return } - + const hotkeyString = eventToHotkeyString(event) if (!hotkeyString) return if (hotkeyString !== editShortcut || @@ -600,7 +613,7 @@ watch( () => isEditing.value, async editing => { await nextTick() - + let checkboxes = tiptapInstanceRef.value?.querySelectorAll('[data-checked]') if (typeof checkboxes === 'undefined' || checkboxes.length === 0) { // For some reason, this works when we check a second time. From 79577c14b71d32154e931d2f8cfd6fb4e023df6c Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 13 Mar 2024 17:07:10 +0100 Subject: [PATCH 36/66] fix(filters): set default filter value to only undone tasks --- frontend/src/services/taskCollection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/services/taskCollection.ts b/frontend/src/services/taskCollection.ts index f83dac2e2..046c3f0d9 100644 --- a/frontend/src/services/taskCollection.ts +++ b/frontend/src/services/taskCollection.ts @@ -17,7 +17,7 @@ export function getDefaultTaskFilterParams(): TaskFilterParams { return { sort_by: ['position', 'id'], order_by: ['asc', 'desc'], - filter: '', + filter: 'done = false', filter_include_nulls: false, filter_timezone: '', s: '', From 15215b30a0a0dd693d5ceb4a213f6d68e93b97e7 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 13 Mar 2024 17:19:15 +0100 Subject: [PATCH 37/66] fix(filters): rework filter popup button --- .../project/partials/filter-popup.vue | 7 - .../components/project/partials/filters.vue | 12 + frontend/src/styles/components/project.scss | 41 +-- frontend/src/views/project/ProjectKanban.vue | 48 +-- frontend/src/views/project/ProjectList.vue | 78 +--- frontend/src/views/project/ProjectTable.vue | 336 +++++++++--------- 6 files changed, 232 insertions(+), 290 deletions(-) diff --git a/frontend/src/components/project/partials/filter-popup.vue b/frontend/src/components/project/partials/filter-popup.vue index 4c4d3eb5c..eaef47288 100644 --- a/frontend/src/components/project/partials/filter-popup.vue +++ b/frontend/src/components/project/partials/filter-popup.vue @@ -1,11 +1,4 @@ @@ -290,7 +290,11 @@ import KanbanCard from '@/components/tasks/partials/kanban-card.vue' import Dropdown from '@/components/misc/dropdown.vue' import DropdownItem from '@/components/misc/dropdown-item.vue' -import {getCollapsedBucketState, saveCollapsedBucketState, type CollapsedBuckets} from '@/helpers/saveCollapsedBucketState' +import { + type CollapsedBuckets, + getCollapsedBucketState, + saveCollapsedBucketState, +} from '@/helpers/saveCollapsedBucketState' import {calculateItemPosition} from '@/helpers/calculateItemPosition' import {isSavedFilter} from '@/services/savedFilter' @@ -322,7 +326,7 @@ const kanbanStore = useKanbanStore() const taskStore = useTaskStore() const projectStore = useProjectStore() -const taskContainerRefs = ref<{[id: IBucket['id']]: HTMLElement}>({}) +const taskContainerRefs = ref<{ [id: IBucket['id']]: HTMLElement }>({}) const bucketLimitInputRef = ref(null) const drag = ref(false) @@ -334,18 +338,18 @@ const bucketToDelete = ref(0) const bucketTitleEditable = ref(false) const newTaskText = ref('') -const showNewTaskInput = ref<{[id: IBucket['id']]: boolean}>({}) +const showNewTaskInput = ref<{ [id: IBucket['id']]: boolean }>({}) const newBucketTitle = ref('') const showNewBucketInput = ref(false) -const newTaskError = ref<{[id: IBucket['id']]: boolean}>({}) +const newTaskError = ref<{ [id: IBucket['id']]: boolean }>({}) const newTaskInputFocused = ref(false) const showSetLimitInput = ref(false) const collapsedBuckets = ref({}) // We're using this to show the loading animation only at the task when updating it -const taskUpdating = ref<{[id: ITask['id']]: boolean}>({}) +const taskUpdating = ref<{ [id: ITask['id']]: boolean }>({}) const oneTaskUpdating = ref(false) const params = ref({ @@ -378,7 +382,7 @@ const bucketDraggableComponentData = computed(() => ({ ], })) const canWrite = computed(() => baseStore.currentProject?.maxRight > Rights.READ) -const project = computed(() => projectId ? projectStore.projects[projectId]: null) +const project = computed(() => projectId ? projectStore.projects[projectId] : null) const buckets = computed(() => kanbanStore.buckets) const loading = computed(() => kanbanStore.isLoading) @@ -497,7 +501,7 @@ async function updateTaskPosition(e) { await taskStore.update(newTask) // Make sure the first and second task don't both get position 0 assigned - if(newTaskIndex === 0 && taskAfter !== null && taskAfter.kanbanPosition === 0) { + if (newTaskIndex === 0 && taskAfter !== null && taskAfter.kanbanPosition === 0) { const taskAfterAfter = newBucket.tasks[newTaskIndex + 2] ?? null const newTaskAfter = klona(taskAfter) // cloning the task to avoid pinia store manipulation newTaskAfter.bucketId = newBucket.id @@ -602,7 +606,7 @@ function updateBuckets(value: IBucket[]) { } // TODO: fix type -function updateBucketPosition(e: {newIndex: number}) { +function updateBucketPosition(e: { newIndex: number }) { // (2) bucket positon is changed dragBucket.value = false @@ -631,19 +635,19 @@ async function saveBucketLimit(bucketId: IBucket['id'], limit: number) { success({message: t('project.kanban.bucketLimitSavedSuccess')}) } -const setBucketLimitCancel = ref(null) +const setBucketLimitCancel = ref(null) async function setBucketLimit(bucketId: IBucket['id'], now: boolean = false) { const limit = parseInt(bucketLimitInputRef.value?.value || '') - + if (setBucketLimitCancel.value !== null) { clearTimeout(setBucketLimitCancel.value) } - + if (now) { return saveBucketLimit(bucketId, limit) } - + setBucketLimitCancel.value = setTimeout(saveBucketLimit, 2500, bucketId, limit) } @@ -739,6 +743,7 @@ $filter-container-height: '1rem - #{$switch-view-height}'; * { opacity: 0; } + &::after { content: ''; position: absolute; @@ -780,6 +785,7 @@ $filter-container-height: '1rem - #{$switch-view-height}'; &:first-of-type { padding-top: .5rem; } + &:last-of-type { padding-bottom: .5rem; } diff --git a/frontend/src/views/project/ProjectList.vue b/frontend/src/views/project/ProjectList.vue index 060b734ca..399f217bf 100644 --- a/frontend/src/views/project/ProjectList.vue +++ b/frontend/src/views/project/ProjectList.vue @@ -5,52 +5,12 @@ view-name="project" > @@ -113,14 +73,14 @@ > - @@ -131,7 +91,7 @@ diff --git a/frontend/src/components/project/partials/filters.vue b/frontend/src/components/project/partials/filters.vue index dab0efdbe..c614af99e 100644 --- a/frontend/src/components/project/partials/filters.vue +++ b/frontend/src/components/project/partials/filters.vue @@ -28,6 +28,7 @@ {{ $t('filters.clear') }} From b3caece2560aa9e8d5461bfd6fa584ebf98c6ff3 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 13 Mar 2024 18:03:49 +0100 Subject: [PATCH 40/66] fix(datepicker): emit date value changes as soon as they happen Flatpickr only returns a change event when the value in the input it's referring to changes. That means it will usually only trigger when the focus is moved out of the input field. This is fine most of the time. However, since we're displaying flatpickr in a popup, the whole html dom instance might get destroyed, before the change event had a chance to fire. In that case, it would not update the date value. To fix this, we're now listening on every change and bubble them up as soon as they happen. Resolves https://community.vikunja.io/t/due-date-confirm-button-not-working/2104 --- .../src/components/input/datepickerInline.vue | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/input/datepickerInline.vue b/frontend/src/components/input/datepickerInline.vue index b8a7a7d5b..eca16d99d 100644 --- a/frontend/src/components/input/datepickerInline.vue +++ b/frontend/src/components/input/datepickerInline.vue @@ -63,6 +63,7 @@
@@ -70,7 +71,7 @@ From 7bf2664e558c5aea47c28e8f15c4cd778b230351 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 13 Mar 2024 19:03:23 +0100 Subject: [PATCH 43/66] fix(filters): persist filters in url This allows us to keep the filters when navigating back from a task or other url. --- .../project/partials/filter-popup.vue | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/project/partials/filter-popup.vue b/frontend/src/components/project/partials/filter-popup.vue index 8f037def4..7d3247a07 100644 --- a/frontend/src/components/project/partials/filter-popup.vue +++ b/frontend/src/components/project/partials/filter-popup.vue @@ -30,21 +30,39 @@ import {computed, ref, watch} from 'vue' import Filters from '@/components/project/partials/filters.vue' -import {type TaskFilterParams} from '@/services/taskCollection' +import {getDefaultTaskFilterParams, type TaskFilterParams} from '@/services/taskCollection' +import {useRouteQuery} from '@vueuse/router' const modelValue = defineModel({}) const value = ref({}) +const filter = useRouteQuery('filter') watch( () => modelValue.value, (modelValue: TaskFilterParams) => { value.value = modelValue + if (value.value.filter !== '' && value.value.filter !== getDefaultTaskFilterParams().filter) { + filter.value = value.value.filter + } + }, + {immediate: true}, +) + +watch( + () => filter.value, + val => { + if (modelValue.value?.filter === val || typeof val === 'undefined') { + return + } + + modelValue.value.filter = val }, {immediate: true}, ) function emitChanges(newValue: TaskFilterParams) { + filter.value = newValue.filter if (modelValue.value?.filter === newValue.filter && modelValue.value?.s === newValue.s) { return } @@ -54,7 +72,7 @@ function emitChanges(newValue: TaskFilterParams) { } const hasFilters = computed(() => { - return value.value.filter !== '' || + return value.value.filter !== '' || value.value.s !== '' }) @@ -73,13 +91,13 @@ const modalOpen = ref(false) $filter-bubble-size: .75rem; .has-filters { position: relative; - + &::after { content: ''; position: absolute; top: math.div($filter-bubble-size, -2); right: math.div($filter-bubble-size, -2); - + width: $filter-bubble-size; height: $filter-bubble-size; border-radius: 100%; From 8ff59d46490aa8c114d203d8cb9e4d1c05896f60 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 13 Mar 2024 19:11:49 +0100 Subject: [PATCH 44/66] fix(task): navigate back to project when the project was the last page in the history the user visited --- frontend/src/views/tasks/TaskDetailView.vue | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/views/tasks/TaskDetailView.vue b/frontend/src/views/tasks/TaskDetailView.vue index f3e274396..12b593e37 100644 --- a/frontend/src/views/tasks/TaskDetailView.vue +++ b/frontend/src/views/tasks/TaskDetailView.vue @@ -25,7 +25,16 @@ v-for="p in projectStore.getAncestors(project)" :key="p.id" > - + + {{ getProjectTitle(p) }} + + {{ getProjectTitle(p) }} Date: Wed, 13 Mar 2024 19:23:02 +0100 Subject: [PATCH 45/66] fix(editor): do not use Tiptap to open links when clicking on them, use the browser native attributes instead It looks like links are opened twice, when the openOnClick option is enabled. That means they will get opened twice when clicking on them. Disabling that option will not fire the click handler and only rely on browser functionality to open links. Resolves https://kolaente.dev/vikunja/vikunja/issues/2155 --- frontend/src/components/input/editor/TipTap.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/input/editor/TipTap.vue b/frontend/src/components/input/editor/TipTap.vue index 0213546f6..af4da5455 100644 --- a/frontend/src/components/input/editor/TipTap.vue +++ b/frontend/src/components/input/editor/TipTap.vue @@ -374,7 +374,7 @@ const editor = useEditor({ Typography, Underline, Link.configure({ - openOnClick: true, + openOnClick: false, validate: (href: string) => /^https?:\/\//.test(href), }), Table.configure({ From 117079bbda9a6b83f6d49061dd2a16ac167cc8e1 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 13 Mar 2024 19:31:43 +0100 Subject: [PATCH 46/66] fix(sentry): do not send api errors to sentry --- frontend/src/sentry.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frontend/src/sentry.ts b/frontend/src/sentry.ts index d48769dbf..32362f80e 100644 --- a/frontend/src/sentry.ts +++ b/frontend/src/sentry.ts @@ -1,6 +1,7 @@ import 'virtual:vite-plugin-sentry/sentry-config' import type {App} from 'vue' import type {Router} from 'vue-router' +import {AxiosError} from 'axios' export default async function setupSentry(app: App, router: Router) { const Sentry = await import('@sentry/vue') @@ -18,5 +19,15 @@ export default async function setupSentry(app: App, router: Router) { }), ], tracesSampleRate: 1.0, + beforeSend(event, hint) { + + if ((typeof hint.originalException?.code !== 'undefined' && + typeof hint.originalException?.message !== 'undefined') + || hint.originalException instanceof AxiosError) { + return null + } + + return event + }, }) } From 8c826c44d2684538d7b59c12bfbe74c6ce6d8bf5 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 13 Mar 2024 19:41:34 +0100 Subject: [PATCH 47/66] fix(webhooks): fire webhooks set on parent projects as well --- pkg/models/listeners.go | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/pkg/models/listeners.go b/pkg/models/listeners.go index ea3a5a462..9809d77c8 100644 --- a/pkg/models/listeners.go +++ b/pkg/models/listeners.go @@ -725,24 +725,36 @@ func (wl *WebhookListener) Handle(msg *message.Message) (err error) { s := db.NewSession() defer s.Close() + parents, err := GetAllParentProjects(s, projectID) + if err != nil { + return err + } + + projectIDs := make([]int64, 0, len(parents)+1) + projectIDs = append(projectIDs, projectID) + + for _, p := range parents { + projectIDs = append(projectIDs, p.ID) + } + ws := []*Webhook{} - err = s.Where("project_id = ?", projectID). + err = s.In("project_id", projectIDs). Find(&ws) if err != nil { return err } - var webhook *Webhook + matchingWebhooks := []*Webhook{} for _, w := range ws { for _, e := range w.Events { if e == wl.EventName { - webhook = w + matchingWebhooks = append(matchingWebhooks, w) break } } } - if webhook == nil { + if len(matchingWebhooks) == 0 { log.Debugf("Did not find any webhook for the %s event for project %d, not sending", wl.EventName, projectID) return nil } @@ -789,11 +801,17 @@ func (wl *WebhookListener) Handle(msg *message.Message) (err error) { } } - err = webhook.sendWebhookPayload(&WebhookPayload{ - EventName: wl.EventName, - Time: time.Now(), - Data: event, - }) + for _, webhook := range matchingWebhooks { + err = webhook.sendWebhookPayload(&WebhookPayload{ + EventName: wl.EventName, + Time: time.Now(), + Data: event, + }) + if err != nil { + return err + } + } + return } From d4605905d3ef32807ae39e8f409753fd5e1e9b7f Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 13 Mar 2024 19:58:24 +0100 Subject: [PATCH 48/66] fix(filters): do not fire filter change immediately Related to https://kolaente.dev/vikunja/vikunja/issues/2194#issuecomment-61081 --- .../components/project/partials/FilterInput.vue | 7 ++++++- .../src/components/project/partials/filters.vue | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/project/partials/FilterInput.vue b/frontend/src/components/project/partials/FilterInput.vue index d668fde47..518d71033 100644 --- a/frontend/src/components/project/partials/FilterInput.vue +++ b/frontend/src/components/project/partials/FilterInput.vue @@ -20,6 +20,7 @@ import { getFilterFieldRegexPattern, LABEL_FIELDS, } from '@/helpers/filters' +import {useDebounceFn} from '@vueuse/core' const { modelValue, @@ -236,6 +237,10 @@ function autocompleteSelect(value) { autocompleteResults.value = [] } + +// The blur from the textarea might happen before the replacement after autocomplete select was done. +// That caused listeners to try and replace values earlier, resulting in broken queries. +const blurDebounced = useDebounceFn(() => emit('blur'), 500)