Add global result frontend
Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
@@ -62,7 +62,7 @@ class CardCommentProvider implements IProvider {
|
||||
if (count($results) < $query->getLimit()) {
|
||||
return SearchResult::complete(
|
||||
$this->l10n->t('Card comments'),
|
||||
$results,
|
||||
$results
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ class DeckProvider implements IProvider {
|
||||
if (count($cardResults) < $query->getLimit()) {
|
||||
return SearchResult::complete(
|
||||
'Deck',
|
||||
$results,
|
||||
$results
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ use OCA\Deck\Db\CardMapper;
|
||||
use OCA\Deck\Search\CommentSearchResultEntry;
|
||||
use OCA\Deck\Search\FilterStringParser;
|
||||
use OCP\Comments\ICommentsManager;
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUserManager;
|
||||
|
||||
class SearchService {
|
||||
|
||||
@@ -45,19 +48,31 @@ class SearchService {
|
||||
private $commentsManager;
|
||||
/** @var FilterStringParser */
|
||||
private $filterStringParser;
|
||||
/** @var IUserManager */
|
||||
private $userManager;
|
||||
/** @var IL10N */
|
||||
private $l10n;
|
||||
/** @var IURLGenerator */
|
||||
private $urlGenerator;
|
||||
|
||||
public function __construct(
|
||||
BoardService $boardService,
|
||||
CardMapper $cardMapper,
|
||||
CardService $cardService,
|
||||
ICommentsManager $commentsManager,
|
||||
FilterStringParser $filterStringParser
|
||||
FilterStringParser $filterStringParser,
|
||||
IUserManager $userManager,
|
||||
IL10N $l10n,
|
||||
IURLGenerator $urlGenerator
|
||||
) {
|
||||
$this->boardService = $boardService;
|
||||
$this->cardMapper = $cardMapper;
|
||||
$this->cardService = $cardService;
|
||||
$this->commentsManager = $commentsManager;
|
||||
$this->filterStringParser = $filterStringParser;
|
||||
$this->userManager = $userManager;
|
||||
$this->l10n = $l10n;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
}
|
||||
|
||||
public function searchCards(string $term, int $limit = null, ?int $cursor = null): array {
|
||||
@@ -89,7 +104,7 @@ class SearchService {
|
||||
$matchedComments = $this->cardMapper->searchComments($boardIds, $this->filterStringParser->parse($term), $limit, $cursor);
|
||||
|
||||
$self = $this;
|
||||
return array_filter(array_map(function ($cardRow) use ($self) {
|
||||
return array_map(function ($cardRow) use ($self) {
|
||||
$comment = $this->commentsManager->get($cardRow['comment_id']);
|
||||
unset($cardRow['comment_id']);
|
||||
$card = Card::fromRow($cardRow);
|
||||
@@ -97,6 +112,6 @@ class SearchService {
|
||||
$user = $this->userManager->get($comment->getActorId());
|
||||
$displayName = $user ? $user->getDisplayName() : '';
|
||||
return new CommentSearchResultEntry($comment->getId(), $comment->getMessage(), $displayName, $card, $this->urlGenerator, $this->l10n);
|
||||
}, $matchedComments));
|
||||
}, $matchedComments);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
<p />
|
||||
</div>
|
||||
</transition>
|
||||
<GlobalSearchResults />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -75,10 +76,12 @@ import { mapState, mapGetters } from 'vuex'
|
||||
import Controls from '../Controls'
|
||||
import Stack from './Stack'
|
||||
import { EmptyContent } from '@nextcloud/vue'
|
||||
import GlobalSearchResults from '../search/GlobalSearchResults'
|
||||
|
||||
export default {
|
||||
name: 'Board',
|
||||
components: {
|
||||
GlobalSearchResults,
|
||||
Controls,
|
||||
Container,
|
||||
Draggable,
|
||||
@@ -178,13 +181,17 @@ export default {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: calc(100vh - 50px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.board {
|
||||
padding-left: $board-spacing;
|
||||
position: relative;
|
||||
height: calc(100% - 44px);
|
||||
overflow-x: scroll;
|
||||
max-height: calc(100% - 44px);
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,12 +26,16 @@
|
||||
|
||||
<template>
|
||||
<AttachmentDragAndDrop v-if="card" :card-id="card.id" class="drop-upload--card">
|
||||
<div :class="{'compact': compactMode, 'current-card': currentCard, 'has-labels': card.labels && card.labels.length > 0, 'is-editing': editing, 'card__editable': canEdit}"
|
||||
<div :class="{'compact': compactMode, 'current-card': currentCard, 'has-labels': card.labels && card.labels.length > 0, 'is-editing': editing, 'card__editable': canEdit, 'card__archived': card.archived }"
|
||||
tag="div"
|
||||
class="card"
|
||||
@click="openCard">
|
||||
<div v-if="standalone" class="card-related">
|
||||
<div :style="{backgroundColor: '#' + board.color}" class="board-bullet" />
|
||||
{{ board.title }} » {{ stack.title }}
|
||||
</div>
|
||||
<div class="card-upper">
|
||||
<h3 v-if="compactMode || isArchived || showArchived || !canEdit">
|
||||
<h3 v-if="compactMode || isArchived || showArchived || !canEdit || standalone">
|
||||
{{ card.title }}
|
||||
</h3>
|
||||
<h3 v-else-if="!editing">
|
||||
@@ -98,6 +102,10 @@ export default {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
standalone: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -114,6 +122,12 @@ export default {
|
||||
...mapGetters([
|
||||
'isArchived',
|
||||
]),
|
||||
board() {
|
||||
return this.$store.getters.boardById(this?.stack?.boardId)
|
||||
},
|
||||
stack() {
|
||||
return this.$store.getters.stackById(this?.card?.stackId)
|
||||
},
|
||||
canEdit() {
|
||||
if (this.currentBoard) {
|
||||
return !this.currentBoard.archived && this.$store.getters.canEdit
|
||||
@@ -233,6 +247,9 @@ export default {
|
||||
&.card__editable .card-controls {
|
||||
margin-right: 0;
|
||||
}
|
||||
&.card__archived {
|
||||
background-color: var(--color-background-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.duedate {
|
||||
@@ -244,6 +261,24 @@ export default {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.card-related {
|
||||
display: flex;
|
||||
padding: 12px;
|
||||
padding-bottom: 0px;
|
||||
color: var(--color-text-maxcontrast);
|
||||
|
||||
.board-bullet {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
margin-top: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.compact {
|
||||
min-height: 44px;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<template>
|
||||
<div v-if="card">
|
||||
<div @click.stop.prevent>
|
||||
<Actions v-if="canEdit && !isArchived">
|
||||
<Actions>
|
||||
<ActionButton v-if="showArchived === false && !isCurrentUserAssigned"
|
||||
icon="icon-user"
|
||||
:close-after-click="true"
|
||||
@@ -43,7 +43,7 @@
|
||||
{{ t('deck', 'Card details') }}
|
||||
</ActionButton>
|
||||
<ActionButton icon="icon-archive" :close-after-click="true" @click="archiveUnarchiveCard()">
|
||||
{{ showArchived ? t('deck', 'Unarchive card') : t('deck', 'Archive card') }}
|
||||
{{ card.archived ? t('deck', 'Unarchive card') : t('deck', 'Archive card') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-if="showArchived === false"
|
||||
icon="icon-delete"
|
||||
|
||||
@@ -73,6 +73,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GlobalSearchResults />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -82,6 +84,7 @@ import Controls from '../Controls'
|
||||
import CardItem from '../cards/CardItem'
|
||||
import { mapGetters } from 'vuex'
|
||||
import moment from '@nextcloud/moment'
|
||||
import GlobalSearchResults from '../search/GlobalSearchResults'
|
||||
|
||||
const FILTER_UPCOMING = 'upcoming'
|
||||
|
||||
@@ -92,6 +95,7 @@ const SUPPORTED_FILTERS = [
|
||||
export default {
|
||||
name: 'Overview',
|
||||
components: {
|
||||
GlobalSearchResults,
|
||||
Controls,
|
||||
CardItem,
|
||||
},
|
||||
|
||||
199
src/components/search/GlobalSearchResults.vue
Normal file
199
src/components/search/GlobalSearchResults.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<!--
|
||||
- @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @author Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @license GNU AGPL version 3 or any later version
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License 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 License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="searchQuery!==''" class="global-search">
|
||||
<h2><RichText :text="t('deck', 'Search for {searchQuery} in all boards')" :arguments="queryStringArgs" /></h2>
|
||||
<Actions>
|
||||
<ActionButton icon="icon-close" @click="$store.commit('setSearchQuery', '')" />
|
||||
</Actions>
|
||||
<div class="search-wrapper">
|
||||
<div v-if="loading || filteredResults.length > 0" class="search-results">
|
||||
<CardItem v-for="card in filteredResults"
|
||||
:id="card.id"
|
||||
:key="card.id"
|
||||
:standalone="true" />
|
||||
<Placeholder v-if="loading" />
|
||||
<InfiniteLoading :identifier="searchQuery" @infinite="infiniteHandler">
|
||||
<div slot="spinner" />
|
||||
<div slot="no-more" />
|
||||
<div slot="no-results">
|
||||
{{ t('deck', 'No results found') }}
|
||||
</div>
|
||||
</InfiniteLoading>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>{{ t('deck', 'No results found') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CardItem from '../cards/CardItem'
|
||||
import { mapState } from 'vuex'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import InfiniteLoading from 'vue-infinite-loading'
|
||||
import RichText from '@juliushaertl/vue-richtext'
|
||||
import Placeholder from './Placeholder'
|
||||
import { Actions, ActionButton } from '@nextcloud/vue'
|
||||
|
||||
const createCancelToken = () => axios.CancelToken.source()
|
||||
|
||||
function search({ query, cursor }) {
|
||||
const cancelToken = createCancelToken()
|
||||
|
||||
const request = async() => axios.get(generateOcsUrl('apps/deck/api/v1.0', 2) + '/search', {
|
||||
cancelToken: cancelToken.token,
|
||||
params: {
|
||||
term: query,
|
||||
limit: 20,
|
||||
cursor,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
request,
|
||||
cancel: cancelToken.cancel,
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'GlobalSearchResults',
|
||||
components: { CardItem, InfiniteLoading, RichText, Placeholder, Actions, ActionButton },
|
||||
data() {
|
||||
return {
|
||||
results: [],
|
||||
cancel: null,
|
||||
loading: false,
|
||||
cursor: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
searchQuery: state => state.searchQuery,
|
||||
}),
|
||||
filteredResults() {
|
||||
const sortFn = (a, b) => a.archived - b.archived || b.lastModified - a.lastModified
|
||||
if (this.$route.params.id) {
|
||||
return this.results.filter((result) => result.relatedBoard.id.toString() !== this.$route.params.id.toString()).sort(sortFn)
|
||||
}
|
||||
return [...this.results].sort(sortFn)
|
||||
},
|
||||
queryStringArgs() {
|
||||
return {
|
||||
searchQuery: this.searchQuery,
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
searchQuery() {
|
||||
this.cursor = null
|
||||
this.loading = true
|
||||
this.search()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
infiniteHandler($state) {
|
||||
this.loading = true
|
||||
this.search().then((data) => {
|
||||
if (data.length) {
|
||||
$state.loaded()
|
||||
} else {
|
||||
$state.complete()
|
||||
}
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
async search() {
|
||||
if (this.cancel) {
|
||||
this.cancel()
|
||||
}
|
||||
const { request, cancel } = await search({ query: this.searchQuery, cursor: this.cursor })
|
||||
this.cancel = cancel
|
||||
const { data } = await request()
|
||||
|
||||
if (this.cursor === null) {
|
||||
this.results = []
|
||||
}
|
||||
if (data.ocs.data.length > 0) {
|
||||
data.ocs.data.forEach((card) => {
|
||||
this.$store.dispatch('addCardData', card)
|
||||
})
|
||||
this.results = [...this.results, ...data.ocs.data]
|
||||
this.cursor = data.ocs.data[data.ocs.data.length - 1].lastModified
|
||||
}
|
||||
return data.ocs.data
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../css/variables.scss';
|
||||
|
||||
.global-search {
|
||||
width: 100%;
|
||||
padding: $board-spacing + $stack-spacing;
|
||||
padding-bottom: 0;
|
||||
overflow: hidden;
|
||||
min-height: 35vh;
|
||||
max-height: 50vh;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
border-top: 1px solid var(--color-border);
|
||||
z-index: 1010;
|
||||
position: relative;
|
||||
|
||||
.action-item.icon-close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
.search-wrapper {
|
||||
overflow: scroll;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
h2::v-deep span {
|
||||
background-color: var(--color-background-dark);
|
||||
padding: 3px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.search-results {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
& > div {
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
&::v-deep .card {
|
||||
width: $stack-width;
|
||||
margin-right: $stack-spacing;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
115
src/components/search/Placeholder.vue
Normal file
115
src/components/search/Placeholder.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="card--placeholder">
|
||||
<svg class="card-placeholder__gradient">
|
||||
<defs>
|
||||
<linearGradient id="card-placeholder__gradient">
|
||||
<stop offset="0%" :stop-color="light">
|
||||
<animate attributeName="stop-color"
|
||||
:values="`${light}; ${light}; ${dark}; ${dark}; ${light}`"
|
||||
dur="2s"
|
||||
repeatCount="indefinite" />
|
||||
</stop>
|
||||
<stop offset="100%" :stop-color="dark">
|
||||
<animate attributeName="stop-color"
|
||||
:values="`${dark}; ${light}; ${light}; ${dark}; ${dark}`"
|
||||
dur="2s"
|
||||
repeatCount="indefinite" />
|
||||
</stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<svg
|
||||
class="card-placeholder__placeholder"
|
||||
:class="{ 'standalone': standalone }"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="url(#card-placeholder__gradient)">
|
||||
<rect class="card-placeholder__placeholder-line-header" :style="{width: `calc(${randWidth()}%)`}" />
|
||||
<rect class="card-placeholder__placeholder-line-one" />
|
||||
<rect class="card-placeholder__placeholder-line-two" :style="{width: `calc(${randWidth()}%)`}" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Placeholder',
|
||||
data() {
|
||||
return {
|
||||
light: null,
|
||||
dark: null,
|
||||
standalone: true,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const styles = getComputedStyle(document.documentElement)
|
||||
this.dark = styles.getPropertyValue('--color-placeholder-dark')
|
||||
this.light = styles.getPropertyValue('--color-placeholder-light')
|
||||
},
|
||||
|
||||
methods: {
|
||||
randWidth() {
|
||||
return Math.floor(Math.random() * 20) + 40
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../css/variables.scss';
|
||||
$clickable-area: 44px;
|
||||
|
||||
.card--placeholder {
|
||||
width: $stack-width;
|
||||
margin-right: $stack-spacing;
|
||||
padding: $card-padding;
|
||||
transition: box-shadow 0.1s ease-in-out;
|
||||
box-shadow: 0 0 2px 0 var(--color-box-shadow);
|
||||
border-radius: var(--border-radius-large);
|
||||
font-size: 100%;
|
||||
margin-bottom: $card-spacing;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.card-placeholder__gradient {
|
||||
position: fixed;
|
||||
height: 0;
|
||||
width: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.card-placeholder__placeholder {
|
||||
width: 100%;
|
||||
&-line-header,
|
||||
&-line-one,
|
||||
&-line-two {
|
||||
width: 100%;
|
||||
height: 1em;
|
||||
x: 0;
|
||||
}
|
||||
&-line-header {
|
||||
visibility: hidden;
|
||||
}
|
||||
&-line-one {
|
||||
y: 5px;
|
||||
}
|
||||
|
||||
&-line-two {
|
||||
y: 25px;
|
||||
}
|
||||
|
||||
&.standalone {
|
||||
.card-placeholder__placeholder-line-header {
|
||||
visibility: visible;
|
||||
y: 5px;
|
||||
}
|
||||
.card-placeholder__placeholder-line-one {
|
||||
y: 40px;
|
||||
}
|
||||
|
||||
.card-placeholder__placeholder-line-two {
|
||||
y: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -21,6 +21,7 @@
|
||||
*/
|
||||
|
||||
import { CardApi } from './../services/CardApi'
|
||||
import moment from 'moment'
|
||||
import Vue from 'vue'
|
||||
|
||||
const apiClient = new CardApi()
|
||||
@@ -86,8 +87,90 @@ export default {
|
||||
return true
|
||||
}
|
||||
|
||||
return card.title.toLowerCase().includes(getters.getSearchQuery.toLowerCase())
|
||||
|| card.description.toLowerCase().includes(getters.getSearchQuery.toLowerCase())
|
||||
let hasMatch = true
|
||||
const matches = getters.getSearchQuery.match(/(?:[^\s"]+|"[^"]*")+/g)
|
||||
|
||||
const filterOutQuotes = (q) => {
|
||||
if (q[0] === '"' && q[q.length - 1] === '"') {
|
||||
return q.substr(1, -1)
|
||||
}
|
||||
return q
|
||||
}
|
||||
for (const match of matches) {
|
||||
let [filter, query] = match.indexOf(':') !== -1 ? match.split(/:(.+)/) : [null, match]
|
||||
|
||||
if (filter === 'title') {
|
||||
hasMatch = hasMatch && card.title.toLowerCase().includes(filterOutQuotes(query).toLowerCase())
|
||||
} else if (filter === 'description') {
|
||||
hasMatch = hasMatch && card.description.toLowerCase().includes(filterOutQuotes(query).toLowerCase())
|
||||
} else if (filter === 'list') {
|
||||
const stack = this.getters.stackById(card.stackId)
|
||||
if (!stack) {
|
||||
return false
|
||||
}
|
||||
hasMatch = hasMatch && stack.title.toLowerCase().includes(filterOutQuotes(query).toLowerCase())
|
||||
} else if (filter === 'tag') {
|
||||
hasMatch = hasMatch && card.labels.findIndex((label) => label.title.toLowerCase().includes(filterOutQuotes(query).toLowerCase())) !== -1
|
||||
} else if (filter === 'date') {
|
||||
const datediffHour = ((new Date(card.duedate) - new Date()) / 3600 / 1000)
|
||||
query = filterOutQuotes(query)
|
||||
switch (query) {
|
||||
case 'overdue':
|
||||
hasMatch = hasMatch && (card.overdue === 3)
|
||||
break
|
||||
case 'today':
|
||||
hasMatch = hasMatch && (datediffHour > 0 && datediffHour <= 24 && card.duedate !== null)
|
||||
break
|
||||
case 'week':
|
||||
hasMatch = hasMatch && (datediffHour > 0 && datediffHour <= 7 * 24 && card.duedate !== null)
|
||||
break
|
||||
case 'month':
|
||||
hasMatch = hasMatch && (datediffHour > 0 && datediffHour <= 30 * 24 && card.duedate !== null)
|
||||
break
|
||||
case 'none':
|
||||
hasMatch = hasMatch && (card.duedate === null)
|
||||
break
|
||||
}
|
||||
|
||||
if (card.duedate === null || !hasMatch) {
|
||||
return false
|
||||
}
|
||||
const comparator = query[0] + (query[1] === '=' ? '=' : '')
|
||||
const isValidComparator = ['<', '<=', '>', '>='].indexOf(comparator) !== -1
|
||||
const parsedCardDate = moment(card.duedate)
|
||||
const parsedDate = moment(query.substr(isValidComparator ? comparator.length : 0))
|
||||
switch (comparator) {
|
||||
case '<':
|
||||
hasMatch = hasMatch && parsedCardDate.isBefore(parsedDate)
|
||||
break
|
||||
case '<=':
|
||||
hasMatch = hasMatch && parsedCardDate.isSameOrBefore(parsedDate)
|
||||
break
|
||||
case '>':
|
||||
hasMatch = hasMatch && parsedCardDate.isAfter(parsedDate)
|
||||
break
|
||||
case '>=':
|
||||
hasMatch = hasMatch && parsedCardDate.isSameOrAfter(parsedDate)
|
||||
break
|
||||
default:
|
||||
hasMatch = hasMatch && parsedCardDate.isSame(parsedDate)
|
||||
break
|
||||
}
|
||||
|
||||
} else if (filter === 'assigned') {
|
||||
hasMatch = hasMatch && card.assignedUsers.findIndex((assignment) => {
|
||||
return assignment.participant.primaryKey.toLowerCase() === filterOutQuotes(query).toLowerCase()
|
||||
|| assignment.participant.displayname.toLowerCase() === filterOutQuotes(query).toLowerCase()
|
||||
}) !== -1
|
||||
} else {
|
||||
hasMatch = hasMatch && (card.title.toLowerCase().includes(filterOutQuotes(match).toLowerCase())
|
||||
|| card.description.toLowerCase().includes(filterOutQuotes(match).toLowerCase()))
|
||||
}
|
||||
if (!hasMatch) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
.sort((a, b) => a.order - b.order || a.createdAt - b.createdAt)
|
||||
},
|
||||
@@ -210,7 +293,7 @@ export default {
|
||||
}
|
||||
|
||||
const updatedCard = await apiClient[call](card)
|
||||
commit('deleteCard', updatedCard)
|
||||
commit('updateCard', updatedCard)
|
||||
},
|
||||
async assignCardToUser({ commit }, { card, assignee }) {
|
||||
const user = await apiClient.assignUser(card.id, assignee.userId, assignee.type)
|
||||
@@ -236,5 +319,14 @@ export default {
|
||||
const updatedCard = await apiClient.updateCard(card)
|
||||
commit('updateCardProperty', { property: 'duedate', card: updatedCard })
|
||||
},
|
||||
|
||||
addCardData({ commit }, cardData) {
|
||||
const card = { ...cardData }
|
||||
commit('addStack', card.relatedStack)
|
||||
commit('addBoard', card.relatedBoard)
|
||||
delete card.relatedStack
|
||||
delete card.relatedBoard
|
||||
commit('addCard', card)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -91,6 +91,9 @@ export default new Vuex.Store({
|
||||
boards: state => {
|
||||
return state.boards
|
||||
},
|
||||
boardById: state => (id) => {
|
||||
return state.boards.find((board) => board.id === id)
|
||||
},
|
||||
assignables: state => {
|
||||
return [
|
||||
...state.assignableUsers.map((user) => ({ ...user, type: 0 })),
|
||||
|
||||
Reference in New Issue
Block a user