feat(search): add search bar #547

This commit is contained in:
Stefan Kofler 2021-05-07 10:22:54 +01:00 committed by Johannes Millan
parent 2ffccada67
commit 36ff649bb5
28 changed files with 602 additions and 28 deletions

View file

@ -2,7 +2,7 @@ import { BASE } from '../e2e.const';
import { NBrowser } from '../n-browser-interface';
const URL = `${BASE}/#/tag/TODAY/daily-summary`;
const ADD_TASK_BTN_SEL = '.action-nav > button:first-child';
const ADD_TASK_BTN_SEL = '.action-nav > button:nth-child(2)';
const ADD_TASK_GLOBAL_SEL = 'add-task-bar.global input';
module.exports = {

View file

@ -1,6 +1,6 @@
import { NBrowser } from '../n-browser-interface';
const TOGGLE_BOOKMARK_BAR_BTN = '.action-nav button:nth-child(2)';
const TOGGLE_BOOKMARK_BAR_BTN = '.action-nav button:nth-child(3)';
const BOOKMARK_BAR_OPTS_BTN = 'bookmark-bar .list-controls button:first-of-type';
const ADD_BOOKMARK_BTN = '.mat-menu-panel .mat-menu-item:first-of-type';

View file

@ -4,7 +4,7 @@ import { NBrowser } from '../n-browser-interface';
const ADD_TASK_INITIAL = 'add-task-bar:not(.global) input';
const ADD_TASK_GLOBAL = 'add-task-bar.global input';
const TASK = 'task';
const ADD_TASK_BTN = '.action-nav > button:first-child';
const ADD_TASK_BTN = '.action-nav > button:nth-child(2)';
const WORK_VIEW_URL = `${BASE}/`;
const READY_TO_WORK_BTN = '.ready-to-work-btn';

View file

@ -40,7 +40,6 @@ $dark12: rgb(51, 51, 51);
$dark16: rgb(52, 52, 52);
$dark24: rgb(56, 56, 56);
$dark-theme-bg: $dark0;
$dark-theme-bg-darker: $dark0;
$dark-theme-bg-slightly-lighter: $dark4;
@ -73,7 +72,6 @@ $dark-theme-extra-border-color: rgba(255, 255, 255, 0.12);
$light-theme-separator-color: rgba(18, 18, 18, 0.3);
$dark-theme-separator-color: rgba(255, 255, 255, 0.3);
$c-accent: mat-css-color-accent();
$c-warn: mat-css-color-warn();
@ -112,7 +110,6 @@ $component-max-width: 800px;
$side-nav-width: 200px;
//$main-header-height: $s*7;
$bar-height-large: 56px;
$bar-height: 48px;
$bar-height-small: 40px;
@ -129,7 +126,7 @@ $z-main-header: 10;
$z-bookmarks: 9;
$z-backdrop: 222;
$z-add-task-bar: 10000;
$z-search-bar: 10000;
// COMPONENTS
// ----------
@ -138,16 +135,15 @@ $standard-note-fg-light: $light-theme-text-color;
$standard-note-bg-dark: $dark-theme-bg-lightest;
$standard-note-fg-dark: #eeeeee;
// ANIMATIONS
// ----------
// https://github.com/material-components/material-components-web/blob/master/packages/mdc-animation/_variables.scss
$ani-standard-timing: cubic-bezier(.4, 0, .2, 1) !default;
$ani-enter-timing: cubic-bezier(0, 0, .2, 1) !default;
$ani-leave-timing: cubic-bezier(.4, 0, 1, 1) !default;
$ani-sharp-timing: cubic-bezier(.4, 0, .6, 1) !default;
$ani-standard-timing: cubic-bezier(0.4, 0, 0.2, 1) !default;
$ani-enter-timing: cubic-bezier(0, 0, 0.2, 1) !default;
$ani-leave-timing: cubic-bezier(0.4, 0, 1, 1) !default;
$ani-sharp-timing: cubic-bezier(0.4, 0, 0.6, 1) !default;
$ease-in-out-quint: cubic-bezier(.38, .04, .35, .96);
$ease-in-out-quint: cubic-bezier(0.38, 0.04, 0.35, 0.96);
$transition-duration-xs: 90ms;
$transition-duration-s: 150ms;
$transition-duration-m: 225ms;

View file

@ -11,11 +11,17 @@
<add-task-bar
(blurred)="layoutService.hideAddTaskBar()"
*ngIf="layoutService.isShowAddTaskBar$|async"
[@blendInOut]
[isElevated]="true"
class="global"
></add-task-bar>
<div *ngIf="(layoutService.isShowAddTaskBar$|async)" @fade class="backdrop"></div>
<search-bar
(blurred)="layoutService.hideSearchBar()"
*ngIf="layoutService.isShowSearchBar$|async"
[isElevated]="true"
class="global"
></search-bar>
<div *ngIf="(layoutService.isShowSearchBar$|async)" @fade class="backdrop"></div>
<mat-sidenav-container [dir]="isRTL ? 'rtl' : 'ltr'">
<mat-sidenav
(closedStart)="layoutService.hideSideNav()"

View file

@ -14,10 +14,10 @@
border-bottom: 1px solid transparent;
@include lightTheme() {
border-color: rgba(0, 0, 0, .15);
border-color: rgba(0, 0, 0, 0.15);
}
@include darkTheme() {
border-color: rgba(255, 255, 255, .15);
border-color: rgba(255, 255, 255, 0.15);
}
&.isNotScrolled {
@ -56,6 +56,10 @@ add-task-bar {
z-index: $z-add-task-bar;
}
search-bar {
z-index: $z-search-bar;
}
main-header {
position: relative;
z-index: $z-main-header;
@ -66,7 +70,6 @@ bookmark-bar {
z-index: $z-bookmarks;
}
.additional-info-panel {
min-width: 300px;
width: 40vw;
@ -84,7 +87,6 @@ bookmark-bar {
}
}
// SIDENAV
// -------
:host ::ng-deep .mat-drawer-content {
@ -102,7 +104,6 @@ mat-sidenav-container {
height: 100%;
}
// sidebars
mat-sidenav ::ng-deep .mat-drawer-inner-container {
@include scrollYImportant();

View file

@ -9,7 +9,6 @@ import {
import { ChromeExtensionInterfaceService } from './core/chrome-extension-interface/chrome-extension-interface.service';
import { ShortcutService } from './core-ui/shortcut/shortcut.service';
import { GlobalConfigService } from './features/config/global-config.service';
import { blendInOutAnimation } from './ui/animations/blend-in-out.ani';
import { LayoutService } from './core-ui/layout/layout.service';
import { IPC } from '../../electron/ipc-events.const';
import { SnackService } from './core/snack/snack.service';
@ -49,7 +48,7 @@ const productivityTip: string[] = w.productivityTips && w.productivityTips[w.ran
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
animations: [blendInOutAnimation, expandAnimation, warpRouteAnimation, fadeAnimation],
animations: [expandAnimation, warpRouteAnimation, fadeAnimation],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements OnDestroy {

View file

@ -44,6 +44,7 @@ import { undoTaskDeleteMetaReducer } from './root-store/meta/undo-task-delete.me
import { InitialDialogModule } from './features/initial-dialog/initial-dialog.module';
import { actionLoggerReducer } from './root-store/meta/action-logger.reducer';
import { SyncModule } from './imex/sync/sync.module';
import { SearchBarModule } from './features/search-bar/search-bar.module';
// NOTE: export required for aot to work
export const createTranslateLoader = (http: HttpClient) =>
@ -74,6 +75,7 @@ export const createTranslateLoader = (http: HttpClient) =>
SyncModule,
InitialDialogModule,
MaterialCssVarsModule.forRoot(),
SearchBarModule,
// External
BrowserModule,

View file

@ -2,9 +2,12 @@ import { Injectable } from '@angular/core';
import {
hideAddTaskBar,
hideNotes,
hideSearchBar,
hideSideNav,
showAddTaskBar,
showSearchBar,
toggleAddTaskBar,
toggleSearchBar,
toggleShowNotes,
toggleSideNav,
} from './store/layout.actions';
@ -13,6 +16,7 @@ import { select, Store } from '@ngrx/store';
import {
LayoutState,
selectIsShowAddTaskBar,
selectIsShowSearchBar,
selectIsShowSideNav,
} from './store/layout.reducer';
import { filter, map, switchMap, withLatestFrom } from 'rxjs/operators';
@ -36,6 +40,9 @@ export class LayoutService {
isShowAddTaskBar$: Observable<boolean> = this._store$.pipe(
select(selectIsShowAddTaskBar),
);
isShowSearchBar$: Observable<boolean> = this._store$.pipe(
select(selectIsShowSearchBar),
);
isNavAlwaysVisible$: Observable<boolean> = this._breakPointObserver
.observe([`(min-width: ${NAV_ALWAYS_VISIBLE}px)`])
.pipe(map((result) => result.matches));
@ -102,6 +109,18 @@ export class LayoutService {
this._store$.dispatch(toggleAddTaskBar());
}
showSearchBar() {
this._store$.dispatch(showSearchBar());
}
hideSearchBar() {
this._store$.dispatch(hideSearchBar());
}
toggleSearchBar() {
this._store$.dispatch(toggleSearchBar());
}
toggleSideNav() {
this._store$.dispatch(toggleSideNav());
}

View file

@ -6,6 +6,12 @@ export const hideAddTaskBar = createAction('[Layout] Hide AddTaskBar');
export const toggleAddTaskBar = createAction('[Layout] Toggle AddTaskBar');
export const showSearchBar = createAction('[Layout] Show SearchBar');
export const hideSearchBar = createAction('[Layout] Hide SearchBar');
export const toggleSearchBar = createAction('[Layout] Toggle SearchBar');
export const hideSideNav = createAction('[Layout] Hide SideBar');
export const toggleSideNav = createAction('[Layout] Toggle SideBar');

View file

@ -1,9 +1,12 @@
import {
hideAddTaskBar,
hideNotes,
hideSearchBar,
hideSideNav,
showAddTaskBar,
showSearchBar,
toggleAddTaskBar,
toggleSearchBar,
toggleShowNotes,
toggleSideNav,
} from './layout.actions';
@ -21,6 +24,7 @@ export interface LayoutState {
isShowAddTaskBar: boolean;
isShowBookmarkBar: boolean;
isShowNotes: boolean;
isShowSearchBar: boolean;
isShowSideNav: boolean;
}
@ -28,6 +32,7 @@ const _initialLayoutState: LayoutState = {
isShowAddTaskBar: false,
isShowBookmarkBar: false,
isShowSideNav: false,
isShowSearchBar: false,
isShowNotes: false,
};
@ -50,6 +55,11 @@ export const selectIsShowNotes = createSelector(
(state) => state.isShowNotes,
);
export const selectIsShowSearchBar = createSelector(
selectLayoutFeatureState,
(state) => state.isShowSearchBar,
);
const _reducer = createReducer<LayoutState>(
_initialLayoutState,
@ -62,6 +72,15 @@ const _reducer = createReducer<LayoutState>(
isShowAddTaskBar: !state.isShowAddTaskBar,
})),
on(showSearchBar, (state) => ({ ...state, isShowSearchBar: true })),
on(hideSearchBar, (state) => ({ ...state, isShowSearchBar: false })),
on(toggleSearchBar, (state) => ({
...state,
isShowSearchBar: !state.isShowSearchBar,
})),
on(hideSideNav, (state) => ({ ...state, isShowSideNav: false })),
on(toggleSideNav, (state) => ({ ...state, isShowSideNav: !state.isShowSideNav })),

View file

@ -40,6 +40,15 @@
</ng-container>
<nav class="action-nav">
<button
(click)="layoutService.showSearchBar()"
[matTooltip]="T.MH.SHOW_SEARCH_BAR|translate"
class="hide-xs"
mat-icon-button
>
<mat-icon>search</mat-icon>
</button>
<button
(click)="layoutService.showAddTaskBar()"
[matTooltip]="T.MH.ADD_NEW_TASK|translate"

View file

@ -119,6 +119,9 @@ export class ShortcutService {
//
// } else if (checkKeyCombo(ev, keys.goToFocusMode)) {
// this._router.navigate(['/focus-view']);
} else if (checkKeyCombo(ev, keys.showSearchBar)) {
this._layoutService.toggleSearchBar();
ev.preventDefault();
} else if (checkKeyCombo(ev, keys.toggleSideNav)) {
this._layoutService.toggleSideNav();
ev.preventDefault();

View file

@ -76,6 +76,7 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfigState = {
openProjectNotes: 'Shift+N',
toggleSideNav: 'Shift+D',
showHelp: '?',
showSearchBar: 'Shift+F',
toggleBookmarks: 'Shift+V',
toggleBacklog: 'b',
goToWorkView: 'w',

View file

@ -96,6 +96,13 @@ export const KEYBOARD_SETTINGS_FORM_CFG: ConfigFormSection<KeyboardConfig> = {
// label: T.GCF.KEYBOARD.SHOW_HELP
// },
// },
{
key: 'showSearchBar',
type: 'keyboard',
templateOptions: {
label: T.GCF.KEYBOARD.SHOW_SEARCH_BAR,
},
},
{
key: 'toggleBookmarks',
type: 'keyboard',

View file

@ -12,6 +12,7 @@ export type KeyboardConfig = Readonly<{
addNewTask: string | null;
globalToggleTaskStart: string | null;
showHelp: string | null;
showSearchBar: string | null;
addNewNote: string | null;
toggleSideNav: string | null;
openProjectNotes: string | null;

View file

@ -0,0 +1,55 @@
<form
#searchForm
@blendInOut
(@blendInOut.done)="onAnimationEvent($event)"
[class.isElevated]="isElevated"
[class.mat-elevation-z3]="!isElevated"
[class.mat-elevation-z4]="isElevated"
class="search-form"
>
<div class="input-wrapper">
<div *ngIf="(isLoading$|async)" class="spinner">
<mat-spinner diameter="50"></mat-spinner>
</div>
<button class="switch-add-to-btn" mat-icon-button type="button">
<mat-icon>search</mat-icon>
</button>
<input
#inputEl
[tabindex]="tabindex"
(blur)="onBlur($event)"
[formControl]="taskSuggestionsCtrl"
[matAutocomplete]="autoEl"
matInput
/>
<button
(click)="closeBtnClose($event)"
*ngIf="isElevated"
class="close-btn"
mat-icon-button
type="button"
>
<mat-icon>close</mat-icon>
</button>
</div>
<div class="info-bar">
<div class="info-text">{{T.F.SEARCH_BAR.INFO|translate}}</div>
</div>
<mat-autocomplete
#autoEl="matAutocomplete"
(optionSelected)="navigateToItem($event?.option?.value)"
autoActiveFirstOption
>
<mat-option
*ngFor="let item of (filteredIssueSuggestions$|async); trackBy:trackByFn"
[value]="item"
>
<mat-icon *ngIf="item.issueType" [svgIcon]="item.issueType|issueIcon"></mat-icon>
<tag *ngIf="item.ctx" [tag]="item.ctx"></tag>
<span>{{item?.titleHighlighted||item?.title}}</span>
</mat-option>
</mat-autocomplete>
</form>

View file

@ -0,0 +1,191 @@
@import '../../../variables';
:host {
display: block;
//overflow: hidden;
text-align: start;
&.global {
position: fixed;
top: $s;
left: $s;
right: $s;
overflow: visible;
transform-origin: left top;
@include mq(xs, max) {
transform: translateX(0) !important;
}
@include mq(xs) {
margin-top: 20px;
max-width: 632px;
width: 80%;
top: $s * 6;
left: 50%;
right: auto;
transform: translateX(-50%);
}
}
}
:host-context(.isTouchOnly).global {
top: auto;
bottom: $s;
}
$short-syntax-bar-height: 28px;
.search-form {
width: 100%;
position: relative;
color: #000000;
border-radius: $card-border-radius;
overflow: hidden;
@include darkTheme {
color: #eeeeee;
}
.input-wrapper {
position: relative;
}
input {
padding-bottom: $short-syntax-bar-height;
display: block;
width: 100%;
font-size: 16px;
box-sizing: border-box;
border-style: solid;
border-width: 0;
outline: 0;
border-color: $c-primary;
padding-right: 48px !important;
padding-left: 48px !important;
line-height: 42px;
color: #000000;
background: #ffffff;
}
input {
@include darkTheme {
background-color: $dark-theme-bg-lighter;
color: $dark-theme-text-color-most-intense;
}
}
&.isElevated input {
@include darkTheme {
background-color: $dark-theme-bg-lightest;
color: $dark-theme-text-color-most-intense;
}
}
}
.spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
}
.is-loading {
padding: $s;
::ng-deep .mat-option-text {
display: flex;
justify-content: center;
}
}
.switch-add-to-btn {
position: absolute;
left: $s;
top: 50%;
transform: translateY(-50%);
z-index: 2;
opacity: 0.4;
margin-top: -0.5 * $short-syntax-bar-height;
&:hover {
opacity: 1;
}
}
.close-btn {
position: absolute;
right: $s/2;
top: 50%;
transform: translateY(-50%);
z-index: 2;
opacity: 0.4;
margin-top: -0.5 * $short-syntax-bar-height;
&:hover {
opacity: 1;
}
@include mq(xs) {
}
}
.info-bar {
padding: 0 1.75 * $s;
height: $short-syntax-bar-height;
line-height: $short-syntax-bar-height;
margin-top: -1 * $short-syntax-bar-height;
color: $light-theme-text-color-muted;
background: #ffffff;
border-top: 1px dashed $light-theme-extra-border-color;
position: relative;
z-index: 1;
//border-bottom-left-radius: $card-border-radius;
//border-bottom-right-radius: $card-border-radius;
pointer-events: none;
@include darkTheme {
background-color: $dark-theme-bg-lightest;
border-color: $dark-theme-extra-border-color;
color: $dark-theme-text-color-muted;
}
}
.info-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.shake-form {
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
}
@keyframes shake {
10%,
90% {
transform: translate3d(-1px, 0, 0);
}
20%,
80% {
transform: translate3d(2px, 0, 0);
}
30%,
50%,
70% {
transform: translate3d(-4px, 0, 0);
}
40%,
60% {
transform: translate3d(4px, 0, 0);
}
}

View file

@ -0,0 +1,192 @@
import {
Component,
ElementRef,
EventEmitter,
Input,
OnDestroy,
Output,
ViewChild,
AfterViewInit,
ChangeDetectionStrategy,
} from '@angular/core';
import { T } from '../../t.const';
import { FormControl } from '@angular/forms';
import { BehaviorSubject, Observable } from 'rxjs';
import { tap, map, take, debounceTime, startWith } from 'rxjs/operators';
import { TaskService } from '../tasks/task.service';
import { Params, Router } from '@angular/router';
import { TODAY_TAG } from '../tag/tag.const';
import { Project } from '../project/project.model';
import { Tag } from '../tag/tag.model';
import { ProjectService } from '../project/project.service';
import { TagService } from '../tag/tag.service';
import { Task } from '../tasks/task.model';
import { blendInOutAnimation } from 'src/app/ui/animations/blend-in-out.ani';
import { AnimationEvent } from '@angular/animations';
import { SearchItem } from './search-bar.model';
@Component({
selector: 'search-bar',
templateUrl: './search-bar.component.html',
styleUrls: ['./search-bar.component.scss'],
animations: [blendInOutAnimation],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SearchBarComponent implements AfterViewInit, OnDestroy {
@Input() isElevated: boolean = false;
@Input() tabindex: number = 0;
@Output() blurred: EventEmitter<any> = new EventEmitter();
@ViewChild('inputEl') inputEl!: ElementRef;
@ViewChild('searchForm') searchForm!: ElementRef;
T: typeof T = T;
isLoading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
taskSuggestionsCtrl: FormControl = new FormControl();
filteredIssueSuggestions$: Observable<SearchItem[]> = new Observable();
private _attachKeyDownHandlerTimeout?: number;
private _tasks: Task[] = [];
private _projects: Project[] = [];
private _tags: Tag[] = [];
private _searchableItems: SearchItem[] = [];
constructor(
private _taskService: TaskService,
private _projectService: ProjectService,
private _tagService: TagService,
private _router: Router,
) {
this._taskService.allTasks$.pipe(take(1)).subscribe((t) => (this._tasks = t));
this._projectService.list$.pipe(take(1)).subscribe((p) => (this._projects = p));
this._tagService.tags$.pipe(take(1)).subscribe((t) => (this._tags = t));
this._searchableItems = this._tasks.map((task) => {
const context = this._getCtxForTaskSuggestion(task);
const item: SearchItem = {
id: task.id,
title: task.title.toLowerCase(),
taskNotes: task.notes.toLowerCase(),
location: SearchBarComponent._getLocation(task),
issueType: task.issueType,
isInBacklog: this._isInBacklog(task),
ctx: {
...context,
icon: (context as Tag).icon || (task.projectId && 'list'),
},
};
return item;
});
}
ngAfterViewInit(): void {
this.filteredIssueSuggestions$ = this.taskSuggestionsCtrl.valueChanges.pipe(
debounceTime(100),
startWith(''),
tap(() => this.isLoading$.next(true)),
map((searchTerm) => this._filter(searchTerm)),
tap(() => this.isLoading$.next(false)),
);
this._attachKeyDownHandlerTimeout = window.setTimeout(() => {
this.inputEl.nativeElement.addEventListener('keydown', (ev: KeyboardEvent) => {
if (ev.key === 'Escape') {
this.blurred.emit();
} else if (
ev.key === 'Enter' &&
(!this.taskSuggestionsCtrl.value ||
typeof this.taskSuggestionsCtrl.value === 'string')
) {
this._shakeSearchForm();
ev.preventDefault();
}
});
});
}
private static _getLocation(task: Task): string {
if (task.projectId) {
return `project/${task.projectId}/tasks`;
} else if (task.tagIds.includes(TODAY_TAG.id)) {
return '/tag/TODAY/tasks';
} else if (task.tagIds[0]) {
return `/tag/${task.tagIds[0]}/tasks`;
} else {
console.warn("Couldn't find task location");
return '/';
}
}
private _getCtxForTaskSuggestion(task: Task): Tag | Project {
if (task.projectId) {
return this._projects.find((project) => project.id === task.projectId) as Project;
} else {
if (!task.tagIds[0]) {
throw new Error('No first tag');
}
return this._tags.find((tag) => tag.id === task.tagIds[0]) as Tag;
}
}
private _isInBacklog(task: Task): boolean {
if (!task.projectId) return false;
const project = this._projects.find((p) => p.id === task.projectId);
return project ? project.backlogTaskIds.includes(task.id) : false;
}
private _filter(searchTerm: string): SearchItem[] {
return this._searchableItems.filter(
(task) =>
task.title.includes(searchTerm.toLowerCase()) ||
task.taskNotes.includes(searchTerm.toLowerCase()),
);
}
private _shakeSearchForm(): void {
this.searchForm.nativeElement.classList.toggle('shake-form');
this.searchForm.nativeElement.onanimationend = () => {
this.searchForm.nativeElement.classList.toggle('shake-form');
};
}
onAnimationEvent(event: AnimationEvent) {
if (event.fromState) {
this.inputEl.nativeElement.focus();
}
}
navigateToItem(item: SearchItem) {
if (!item) return;
const params: Params = {
queryParams: {
highlightItem: item.id,
isInBacklog: item.isInBacklog,
},
};
this._router.navigate([item.location], params);
this.blurred.emit();
}
onBlur(ev: FocusEvent) {
const relatedTarget: HTMLElement = ev.relatedTarget as HTMLElement;
if (relatedTarget?.className.includes('switch-add-to-btn')) {
this.inputEl.nativeElement.focus();
} else if (!relatedTarget?.className.includes('mat-option')) {
this.blurred.emit(ev);
}
}
trackByFn(i: number, item: SearchItem) {
return item.id;
}
closeBtnClose(ev: Event) {
this.blurred.emit(ev);
}
ngOnDestroy(): void {
if (this._attachKeyDownHandlerTimeout) {
window.clearTimeout(this._attachKeyDownHandlerTimeout);
}
}
}

View file

@ -0,0 +1,19 @@
import { Project } from '../project/project.model';
import { Tag } from '../tag/tag.model';
import { IssueProviderKey } from '../issue/issue.model';
export interface SearchItem {
id: string;
title: string;
taskNotes: string;
// for the navigation
location: string;
isInBacklog: boolean;
// for the icons
issueType: IssueProviderKey | null;
ctx: Tag | Project;
// jira only
titleHighlighted?: string;
}

View file

@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SearchBarComponent } from './search-bar.component';
import { IssueModule } from '../issue/issue.module';
import { UiModule } from 'src/app/ui/ui.module';
import { TagModule } from '../tag/tag.module';
@NgModule({
declarations: [SearchBarComponent],
imports: [CommonModule, IssueModule, UiModule, TagModule],
exports: [SearchBarComponent],
})
export class SearchBarModule {}

View file

@ -1,5 +1,6 @@
<form
(submit)="addTask()"
@blendInOut
[class.isElevated]="isElevated"
[class.mat-elevation-z3]="!isElevated"
[class.mat-elevation-z4]="isElevated"

View file

@ -40,6 +40,7 @@ import { Tag } from '../../tag/tag.model';
import { Project } from '../../project/project.model';
import { shortSyntaxToTags } from './short-syntax-to-tags.util';
import { slideAnimation } from '../../../ui/animations/slide.ani';
import { blendInOutAnimation } from 'src/app/ui/animations/blend-in-out.ani';
import { fadeAnimation } from '../../../ui/animations/fade.ani';
import { SS_TODO_TMP } from '../../../core/persistence/ls-keys.const';
@ -48,7 +49,7 @@ import { SS_TODO_TMP } from '../../../core/persistence/ls-keys.const';
templateUrl: './add-task-bar.component.html',
styleUrls: ['./add-task-bar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [slideAnimation, fadeAnimation],
animations: [blendInOutAnimation, slideAnimation, fadeAnimation],
})
export class AddTaskBarComponent implements AfterViewInit, OnDestroy {
@Input() isAddToBacklog: boolean = false;

View file

@ -44,6 +44,7 @@ import { DialogEditTagsForTaskComponent } from '../../tag/dialog-edit-tags/dialo
import { WorkContextService } from '../../work-context/work-context.service';
import { environment } from '../../../../environments/environment';
import { throttle } from 'helpful-decorators';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'task',
@ -136,6 +137,7 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit {
private readonly _cd: ChangeDetectorRef,
private readonly _projectService: ProjectService,
public readonly workContextService: WorkContextService,
private _route: ActivatedRoute,
@Attribute('showParentTitle') private showParentTitle: string,
) {}
@ -220,6 +222,14 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit {
setTimeout(() => {
this.focusTitleForEdit();
});
} else {
this._route.queryParams.subscribe((params) => {
window.setTimeout(() => {
if (params.highlightItem === this.task.id) {
this.focusSelfElement();
}
}, 200);
});
}
}

View file

@ -9,6 +9,7 @@ import {
Renderer2,
ViewChild,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { fromEvent, Subscription } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { isTouchOnly } from '../../../util/is-touch';
@ -35,7 +36,7 @@ export class SplitComponent implements AfterViewInit {
private _isDrag: boolean = false;
private _isViewInitialized: boolean = false;
constructor(private _renderer: Renderer2) {}
constructor(private _renderer: Renderer2, private route: ActivatedRoute) {}
@Input() set splitPos(pos: number) {
if (pos !== this.pos) {
@ -53,6 +54,14 @@ export class SplitComponent implements AfterViewInit {
this._updatePos(this.pos, false);
this._renderer.addClass(this.splitTopEl, ANIMATABLE_CLASS);
this._renderer.addClass(this.splitBottomEl, ANIMATABLE_CLASS);
this.route.queryParams.subscribe((params) => {
window.setTimeout(() => {
if (params.isInBacklog === 'true') {
this._show();
}
});
});
}
toggle() {
@ -126,6 +135,10 @@ export class SplitComponent implements AfterViewInit {
this._updatePos(percentage);
}
private _show() {
this._updatePos(50);
}
private _updatePos(pos: number | undefined, isWasOutsideChange: boolean = false) {
if (typeof pos !== 'number') {
throw new Error('Invalid pos');

View file

@ -891,6 +891,9 @@ const T = {
TITLE: 'F.WORKLOG.WEEK.TITLE',
},
},
SEARCH_BAR: {
INFO: 'F.SEARCH_BAR.INFO',
},
},
FILE_IMEX: {
EXPORT_DATA: 'FILE_IMEX.EXPORT_DATA',
@ -964,6 +967,7 @@ const T = {
GO_TO_TIMELINE: 'GCF.KEYBOARD.GO_TO_TIMELINE',
GO_TO_WORK_VIEW: 'GCF.KEYBOARD.GO_TO_WORK_VIEW',
HELP: 'GCF.KEYBOARD.HELP',
SHOW_SEARCH_BAR: 'GCF.KEYBOARD.SHOW_SEARCH_BAR',
MOVE_TASK_DOWN: 'GCF.KEYBOARD.MOVE_TASK_DOWN',
MOVE_TASK_UP: 'GCF.KEYBOARD.MOVE_TASK_UP',
MOVE_TO_BACKLOG: 'GCF.KEYBOARD.MOVE_TO_BACKLOG',
@ -1125,6 +1129,7 @@ const T = {
TOGGLE_SHOW_NOTES: 'MH.TOGGLE_SHOW_NOTES',
TOGGLE_TRACK_TIME: 'MH.TOGGLE_TRACK_TIME',
WORKLOG: 'MH.WORKLOG',
SHOW_SEARCH_BAR: 'MH.SHOW_SEARCH_BAR',
},
PDS: {
BACK: 'PDS.BACK',

View file

@ -6,26 +6,26 @@ export const blendInOutAnimation = [
transition(':enter', [
style({
opacity: 0,
transform: 'scaleY(0) translateX(-50%)',
transform: 'scaleY(0)',
}),
animate(
ANI_STANDARD_TIMING,
style({
opacity: 1,
transform: 'scaleY(1) translateX(-50%)',
transform: 'scaleY(1)',
}),
),
]), // void => *
transition(':leave', [
style({
opacity: 1,
transform: 'scaleY(1) translateX(-50%)',
transform: 'scaleY(1)',
}),
animate(
ANI_STANDARD_TIMING,
style({
opacity: 0,
transform: 'scaleY(0) translateX(-50%)',
transform: 'scaleY(0)',
}),
),
]),

View file

@ -886,6 +886,9 @@
"NO_DATA": "No tasks this week yet.",
"TITLE": "Title"
}
},
"SEARCH_BAR": {
"INFO": "Search for task or task description"
}
},
"FILE_IMEX": {
@ -959,6 +962,7 @@
"GO_TO_TIMELINE": "Go to Timeline",
"GO_TO_WORK_VIEW": "Go to Work View",
"HELP": "<p>Here you can configure all keyboard shortcuts.</p> <p>Click on the text input and enter the desired keyboard combination. Hit enter to save and Escape to abort.</p> <p>There are three types of shortcuts:</p> <ul> <li> <strong>Global shortcuts:</strong> When the app is running it will trigger the action from every other application. </li> <li> <strong>Application level shortcuts:</strong> Will trigger from every screen of the application, but not if you're currently editing a text field. </li> <li> <strong>Task level shortcuts:</strong> They will only trigger if you have selected a task via mouse or keyboard and usually trigger an action specifically related to that one task. </li> </ul><p>You can <strong>press Escape to remove a shortcut.</strong>",
"SHOW_SEARCH_BAR": "Show search bar",
"MOVE_TASK_DOWN": "Move Task down in List",
"MOVE_TASK_UP": "Move Task up in List",
"MOVE_TO_BACKLOG": "Move Task to Task Backlog",
@ -1117,7 +1121,8 @@
"TOGGLE_SHOW_BOOKMARKS": "Show/Hide Bookmarks",
"TOGGLE_SHOW_NOTES": "Show/Hide Project Notes",
"TOGGLE_TRACK_TIME": "Start/Stop tracking time",
"WORKLOG": "Worklog"
"WORKLOG": "Worklog",
"SHOW_SEARCH_BAR": "Show Search Bar"
},
"PDS": {
"BACK": "Wait I forgot something!",