diff --git a/.eslintrc.json b/.eslintrc.json
index ef5bc12588d..47d3a955e97 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -66,7 +66,7 @@
"leadingUnderscore": "allowSingleOrDouble",
"trailingUnderscore": "allow",
"filter": {
- "regex": "(should)|@tags",
+ "regex": "(should)|@tags|\\d+",
"match": false
}
},
diff --git a/angular.json b/angular.json
index 31156dd92f5..e428038adb8 100644
--- a/angular.json
+++ b/angular.json
@@ -36,7 +36,7 @@
"src/manifest.json",
"src/static"
],
- "styles": ["src/styles.scss"],
+ "styles": ["src/styles.scss", "node_modules/@ctrl/ngx-emoji-mart/picker.css"],
"scripts": [],
"webWorkerTsConfig": "src/tsconfig.worker.json",
"browser": "src/main.ts",
@@ -205,7 +205,7 @@
"tsConfig": "src/tsconfig.spec.json",
"preserveSymlinks": true,
"karmaConfig": "src/karma.conf.js",
- "styles": ["src/styles.scss"],
+ "styles": ["src/styles.scss", "node_modules/@ctrl/ngx-emoji-mart/picker.css"],
"scripts": [],
"assets": ["src/favicon.ico", "src/assets", "src/manifest.json"]
}
diff --git a/package-lock.json b/package-lock.json
index e5bbc39fb7d..173312d5573 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "10.0.11",
"license": "MIT",
"dependencies": {
+ "@ctrl/ngx-emoji-mart": "^9.2.0",
"electron-dl": "^3.5.2",
"electron-localshortcut": "^3.2.1",
"electron-log": "^5.1.2",
@@ -235,6 +236,7 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
@@ -511,6 +513,8 @@
},
"node_modules/@angular-devkit/build-angular/node_modules/rxjs": {
"version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -547,6 +551,7 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
@@ -623,6 +628,8 @@
},
"node_modules/@angular-devkit/core/node_modules/rxjs": {
"version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -652,6 +659,7 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
@@ -1096,6 +1104,7 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
@@ -1294,7 +1303,6 @@
"version": "18.1.4",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-18.1.4.tgz",
"integrity": "sha512-+N3oWYFubT3GdCkBfD/CmH4DGjr/fGFQZChWbph2ZuPpK7JYNgfyvXS4SjLtdL4WTjjBevBTgR70GyLH/5EbKA==",
- "dev": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -3372,6 +3380,28 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
+ "node_modules/@ctrl/ngx-emoji-mart": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/@ctrl/ngx-emoji-mart/-/ngx-emoji-mart-9.2.0.tgz",
+ "integrity": "sha512-q8B7DiXPfyTe+VOGO6Ix7eh5gKCuADxAsud2xXRTlECTfchoKGTmirSczyjaxIKE/xmB+/ZsnkpthZqUM7SAJQ==",
+ "license": "MIT",
+ "dependencies": {
+ "rxjs": "7.8.1",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "@angular/core": ">=15.0.0-0"
+ }
+ },
+ "node_modules/@ctrl/ngx-emoji-mart/node_modules/rxjs": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
"node_modules/@ctrl/tinycolor": {
"version": "4.1.0",
"dev": true,
@@ -22139,7 +22169,8 @@
},
"node_modules/rxjs": {
"version": "6.6.7",
- "dev": true,
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
+ "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^1.9.0"
@@ -22150,7 +22181,8 @@
},
"node_modules/rxjs/node_modules/tslib": {
"version": "1.14.1",
- "dev": true,
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"license": "0BSD"
},
"node_modules/sade": {
@@ -23981,8 +24013,7 @@
"node_modules/tslib": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
- "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
- "dev": true
+ "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
},
"node_modules/tuf-js": {
"version": "2.2.1",
@@ -24601,6 +24632,7 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
@@ -25575,7 +25607,6 @@
},
"node_modules/zone.js": {
"version": "0.14.7",
- "dev": true,
"license": "MIT"
},
"tools/schematics": {
diff --git a/package.json b/package.json
index 020891c3a81..e2bc3baf1ac 100644
--- a/package.json
+++ b/package.json
@@ -106,6 +106,7 @@
}
],
"dependencies": {
+ "@ctrl/ngx-emoji-mart": "^9.2.0",
"electron-dl": "^3.5.2",
"electron-localshortcut": "^3.2.1",
"electron-log": "^5.1.2",
diff --git a/src/app/core-ui/side-nav/side-nav-item/side-nav-item.component.html b/src/app/core-ui/side-nav/side-nav-item/side-nav-item.component.html
index 644aac2e217..554d8f42ac2 100644
--- a/src/app/core-ui/side-nav/side-nav-item/side-nav-item.component.html
+++ b/src/app/core-ui/side-nav/side-nav-item/side-nav-item.component.html
@@ -15,7 +15,17 @@
mat-menu-item
>
{{ workContext().taskIds.length }}
- {{ workContext().icon || defaultIcon() }}
+
+
+ {{ workContext().icon || defaultIcon() }}
+
{{ workContext().title }}
diff --git a/src/app/core-ui/side-nav/side-nav-item/side-nav-item.component.scss b/src/app/core-ui/side-nav/side-nav-item/side-nav-item.component.scss
index fba5e15cc93..e2cc6d1caa6 100644
--- a/src/app/core-ui/side-nav/side-nav-item/side-nav-item.component.scss
+++ b/src/app/core-ui/side-nav/side-nav-item/side-nav-item.component.scss
@@ -36,6 +36,15 @@
&.isHidden {
display: none !important;
}
+
+ button.mat-mdc-menu-item {
+ ngx-emoji {
+ display: block;
+ width: 40px;
+ height: 40px;
+ top: 5px;
+ }
+ }
}
// color bar left styles
diff --git a/src/app/core-ui/side-nav/side-nav-item/side-nav-item.component.ts b/src/app/core-ui/side-nav/side-nav-item/side-nav-item.component.ts
index 2d846e58a1e..6d64810442b 100644
--- a/src/app/core-ui/side-nav/side-nav-item/side-nav-item.component.ts
+++ b/src/app/core-ui/side-nav/side-nav-item/side-nav-item.component.ts
@@ -17,6 +17,8 @@ import { Project } from '../../../features/project/project.model';
import { WorkContextMenuComponent } from '../../work-context-menu/work-context-menu.component';
import { ContextMenuComponent } from '../../../ui/context-menu/context-menu.component';
import { CdkDragPlaceholder } from '@angular/cdk/drag-drop';
+import { EmojiComponent } from '@ctrl/ngx-emoji-mart/ngx-emoji';
+import { CommonModule } from '@angular/common';
@Component({
selector: 'side-nav-item',
@@ -28,6 +30,8 @@ import { CdkDragPlaceholder } from '@angular/cdk/drag-drop';
WorkContextMenuComponent,
ContextMenuComponent,
CdkDragPlaceholder,
+ EmojiComponent,
+ CommonModule,
],
templateUrl: './side-nav-item.component.html',
styleUrl: './side-nav-item.component.scss',
diff --git a/src/app/features/config/config.module.ts b/src/app/features/config/config.module.ts
index 439ea2d8f51..b1b9e10cbab 100644
--- a/src/app/features/config/config.module.ts
+++ b/src/app/features/config/config.module.ts
@@ -20,6 +20,8 @@ import { MatSliderModule } from '@angular/material/slider';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatInputModule } from '@angular/material/input';
import { TranslateModule } from '@ngx-translate/core';
+import { SkinComponent } from '@ctrl/ngx-emoji-mart';
+import { EmojiComponent } from '@ctrl/ngx-emoji-mart/ngx-emoji';
@NgModule({
imports: [
@@ -62,6 +64,8 @@ import { TranslateModule } from '@ngx-translate/core';
MatSlideToggleModule,
MatInputModule,
TranslateModule,
+ EmojiComponent,
+ SkinComponent,
],
declarations: [
ConfigSectionComponent,
diff --git a/src/app/features/config/icon-input/icon-input.component.html b/src/app/features/config/icon-input/icon-input.component.html
index c25c64a16ab..1b0da3e6493 100644
--- a/src/app/features/config/icon-input/icon-input.component.html
+++ b/src/app/features/config/icon-input/icon-input.component.html
@@ -1,7 +1,16 @@
-{{ formControl.value }}
+
+
+ {{ formControl.value }}
+
+
+
+
+
+
+ {{emojiId}}
+
{
filteredIcons: string[] = [];
+ filteredEmojis: string[] = [];
+ i18n = {
+ skintones: {
+ 1: 'Default Skin Tone',
+ 2: 'Light Skin Tone',
+ 3: 'Medium-Light Skin Tone',
+ 4: 'Medium Skin Tone',
+ 5: 'Medium-Dark Skin Tone',
+ 6: 'Dark Skin Tone',
+ },
+ };
+ chosenSkin = Number(localStorage.getItem('emoji-mart.skin')) || 1;
get type(): string {
return this.to.type || 'text';
@@ -21,11 +34,15 @@ export class IconInputComponent extends FieldType {
}
onInputValueChange(val: string): void {
- const arr = MATERIAL_ICONS.filter(
+ const iconsResult = MATERIAL_ICONS.filter(
(icoStr) => icoStr && icoStr.toLowerCase().includes(val.toLowerCase()),
);
- arr.length = Math.min(150, arr.length);
- this.filteredIcons = arr;
+ iconsResult.length = Math.min(150, iconsResult.length);
+ this.filteredIcons = iconsResult;
+ this.filteredEmojis =
+ this.emojiSearch
+ .search(val, undefined, 15)
+ ?.map(({ id }) => `emoji:${id}:${this.chosenSkin}`) || [];
if (!val) {
this.formControl.setValue('');
@@ -36,6 +53,30 @@ export class IconInputComponent extends FieldType {
this.formControl.setValue(icon);
}
+ isEmoji(): boolean {
+ return Boolean(this.formControl.value?.startsWith('emoji:'));
+ }
+
+ changeSkin(newSkin: number): void {
+ this.chosenSkin = newSkin;
+ if (!this.isEmoji()) {
+ return;
+ }
+
+ const currentEmoji = this.formControl.value as string;
+ if (currentEmoji.lastIndexOf(':') === currentEmoji.indexOf(':')) {
+ this.formControl.setValue(`${currentEmoji}:${newSkin}`);
+ } else {
+ this.formControl.setValue(
+ currentEmoji.substring(0, currentEmoji.lastIndexOf(':')) + `:${newSkin}`,
+ );
+ }
+ }
+
+ constructor(private emojiSearch: EmojiSearch) {
+ super();
+ }
+
// onKeyDown(ev: KeyboardEvent): void {
// if (ev.key === 'Enter') {
// const ico = (ev as any)?.target?.value;
diff --git a/src/app/features/tag/tag.module.ts b/src/app/features/tag/tag.module.ts
index f5daf1fb80b..86a1065cd1d 100644
--- a/src/app/features/tag/tag.module.ts
+++ b/src/app/features/tag/tag.module.ts
@@ -10,6 +10,7 @@ import { TagListComponent } from './tag-list/tag-list.component';
import { DialogEditTagsForTaskComponent } from './dialog-edit-tags/dialog-edit-tags-for-task.component';
import { TagComponent } from './tag/tag.component';
import { IssueModule } from '../issue/issue.module';
+import { EmojiComponent } from '@ctrl/ngx-emoji-mart/ngx-emoji';
@NgModule({
imports: [
@@ -19,6 +20,7 @@ import { IssueModule } from '../issue/issue.module';
StoreModule.forFeature(TAG_FEATURE_NAME, tagReducer),
EffectsModule.forFeature([TagEffects]),
IssueModule,
+ EmojiComponent,
],
declarations: [
TagListComponent,
diff --git a/src/app/features/tag/tag/tag.component.html b/src/app/features/tag/tag/tag.component.html
index 6f4dee008fa..0206071ab15 100644
--- a/src/app/features/tag/tag/tag.component.html
+++ b/src/app/features/tag/tag/tag.component.html
@@ -1,8 +1,18 @@
-{{tag().icon}}
+ [emoji]="{id: tag().icon.split(':')[1]}"
+ [skin]="tag().icon.split(':')[2]"
+ set="apple"
+ size="15"
+>
+
+ {{tag().icon}}
+
{
it('should parse scheduled date using local time zone when unspecified', () => {
const t = {
...TASK,
- title: '@2024-10-12T13:37',
+ title: '@2030-10-12T13:37',
};
const plannedTimestamp = getPlannedDateTimestampFromShortSyntaxReturnValue(t);
expect(checkIfCorrectDateAndTime(plannedTimestamp, 'saturday', 13, 37)).toBeTrue();
diff --git a/src/styles/components/_overwrite-material.scss b/src/styles/components/_overwrite-material.scss
index 3a91a3b13af..deb34f99aee 100644
--- a/src/styles/components/_overwrite-material.scss
+++ b/src/styles/components/_overwrite-material.scss
@@ -125,6 +125,8 @@ mat-icon.mat-icon[svgicon] {
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
+ display: flex;
+ align-items: center;
}
// CDK DRAG