mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-25 17:03:52 +00:00
feat(search): add search bar #547
This commit is contained in:
parent
2ffccada67
commit
36ff649bb5
28 changed files with 602 additions and 28 deletions
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 })),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
55
src/app/features/search-bar/search-bar.component.html
Normal file
55
src/app/features/search-bar/search-bar.component.html
Normal 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>
|
||||
191
src/app/features/search-bar/search-bar.component.scss
Normal file
191
src/app/features/search-bar/search-bar.component.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
192
src/app/features/search-bar/search-bar.component.ts
Normal file
192
src/app/features/search-bar/search-bar.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/app/features/search-bar/search-bar.model.ts
Normal file
19
src/app/features/search-bar/search-bar.model.ts
Normal 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;
|
||||
}
|
||||
13
src/app/features/search-bar/search-bar.module.ts
Normal file
13
src/app/features/search-bar/search-bar.module.ts
Normal 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 {}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<form
|
||||
(submit)="addTask()"
|
||||
@blendInOut
|
||||
[class.isElevated]="isElevated"
|
||||
[class.mat-elevation-z3]="!isElevated"
|
||||
[class.mat-elevation-z4]="isElevated"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)',
|
||||
}),
|
||||
),
|
||||
]),
|
||||
|
|
|
|||
|
|
@ -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!",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue