Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhance work tracker to use the new getCards #1966

Merged
merged 13 commits into from
Dec 23, 2024
57 changes: 34 additions & 23 deletions packages/boxel-ui/addon/src/components/drag-and-drop/index.gts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type DndItem = Record<string, any>;

export interface DndKanbanBoardArgs<DndColumn> {
columns: DndColumn[];
displayCard?: (card: DndItem) => boolean;
isLoading?: boolean;
onMove?: (
draggedCard: DndItem,
Expand Down Expand Up @@ -153,6 +154,14 @@ export default class DndKanbanBoard extends Component<
this.draggedCard = null;
}

@action
displayCard(card: DndItem): boolean {
if (this.args.displayCard) {
return this.args.displayCard(card);
}
return true;
}

<template>
{{#if this.areModifiersLoaded}}
<div class='draggable-container' {{on 'dragend' this.onDragEnd}}>
Expand All @@ -173,29 +182,31 @@ export default class DndKanbanBoard extends Component<

<div class='column-drop-zone'>
{{#each column.cards as |card|}}
<div
class='draggable-card {{if @isLoading "is-loading"}}'
{{this.DndSortableItemModifier
group='cards'
data=(hash item=card parent=column)
onDrop=this.moveCard
isOnTargetClass='is-on-target'
onDragStart=(fn this.onDragStart card)
}}
>
{{#if (and @isLoading (eq card this.draggedCard))}}
<div class='overlay'></div>
{{yield card column to='card'}}
<LoadingIndicator
width='18'
height='18'
@color='var(--boxel-light)'
class='loader'
/>
{{else}}
{{yield card column to='card'}}
{{/if}}
</div>
{{#if (this.displayCard card)}}
<div
class='draggable-card {{if @isLoading "is-loading"}}'
{{this.DndSortableItemModifier
group='cards'
data=(hash item=card parent=column)
onDrop=this.moveCard
isOnTargetClass='is-on-target'
onDragStart=(fn this.onDragStart card)
}}
>
{{#if (and @isLoading (eq card this.draggedCard))}}
<div class='overlay'></div>
{{yield card column to='card'}}
<LoadingIndicator
width='18'
height='18'
@color='var(--boxel-light)'
class='loader'
/>
{{else}}
{{yield card column to='card'}}
{{/if}}
</div>
{{/if}}
{{/each}}
</div>
</div>
Expand Down
3 changes: 2 additions & 1 deletion packages/experiments-realm/productivity/filter-dropdown.gts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface FilterDropdownSignature {
selected: any;
onChange: (value: any) => void;
onClose: () => boolean | undefined;
isLoading?: boolean;
};
Blocks: {
default: [any];
Expand All @@ -24,7 +25,7 @@ export class FilterDropdown extends GlimmerComponent<FilterDropdownSignature> {
@options={{@options}}
@selected={{@selected}}
@onChange={{@onChange}}
@triggerComponent={{FilterTrigger}}
@triggerComponent={{(component FilterTrigger isLoading=@isLoading)}}
@initiallyOpened={{true}}
@searchEnabled={{true}}
@searchField={{@searchField}}
Expand Down
13 changes: 11 additions & 2 deletions packages/experiments-realm/productivity/filter-trigger.gts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import GlimmerComponent from '@glimmer/component';
import { IconButton } from '@cardstack/boxel-ui/components';
import ListFilter from '@cardstack/boxel-icons/list-filter';

interface TriggerSignature {
Args: {};
Args: {
isLoading?: boolean;
};
Element: HTMLDivElement;
}

export class FilterTrigger extends GlimmerComponent<TriggerSignature> {
<template>
<div class='filter-trigger'>
<IconButton @icon={{ListFilter}} width='13px' height='13px' />
<span class='filter-trigger-text'>Filter</span>
<span class='filter-trigger-text'>
{{#if @isLoading}}
Loading...
{{else}}
Filter
{{/if}}
</span>
</div>

<style scoped>
Expand Down
121 changes: 37 additions & 84 deletions packages/experiments-realm/productivity/task-cards-resource.gts
Original file line number Diff line number Diff line change
@@ -1,118 +1,71 @@
import { getCards } from '@cardstack/runtime-common';
import { tracked } from '@glimmer/tracking';
import { DndColumn, DndItem } from '@cardstack/boxel-ui/components';
import { type Query } from '@cardstack/runtime-common';
import { restartableTask } from 'ember-concurrency';
import { TaskStatusField, Task } from './task';
import type { LooseyGooseyData } from '../loosey-goosey';
import { DndColumn } from '@cardstack/boxel-ui/components';
import { CardDef } from 'https://cardstack.com/base/card-api';

import { isEqual } from 'lodash';
import { Resource } from 'ember-resources';

interface Args {
named: {
query: Query | undefined;
realm: string;
cards: CardDef[];
hasColumnKey: (card: any, key: string) => boolean;
columnKeys: string[];
};
}

// This is a resource because we have to consider 3 data mechanism
// 1. the reactivity of the query. Changes in query should trigger server fetch
// 2. the drag and drop of cards. When dragging and dropping, we should NOT trigger a server fetch
// but rather just update the local data structure
// 3. When we trigger a server fetch, we need to maintain the sort order of the cards.
// Currently, we don't have any mechanism to maintain the sort order but this is good enough for now
// This is a resource because we have to
// 1. to hold state of cards inside of the kanban board
// 2. to order cards that are newly added to the kanban board
class TaskCollection extends Resource<Args> {
@tracked private data: Map<string, DndColumn> = new Map();
@tracked private order: Map<string, string[]> = new Map();
@tracked private query: Query | undefined = undefined;
hasColumnKey?: (card: CardDef, key: string) => boolean = undefined;

private run = restartableTask(async (query: Query, realm: string) => {
let staticQuery = getCards(query, [realm]);
await staticQuery.loaded;
let cards = staticQuery.instances as Task[];
this.commit(cards); //update stale data
commit(cards: CardDef[], columnKeys: string[]) {
columnKeys.forEach((key: string) => {
let currentColumn = this.data.get(key);
let cardsForStatus = cards.filter((card) => {
return this.hasColumnKey ? this.hasColumnKey(card, key) : false;
});

this.query = query;
});

queryHasChanged(query: Query) {
return !isEqual(this.query, query);
}

commit(cards: Task[]) {
TaskStatusField.values?.map((status: LooseyGooseyData) => {
let statusLabel = status.label;
let cardIdsFromOrder = this.order.get(statusLabel);
let newCards: Task[] = [];
if (cardIdsFromOrder) {
newCards = cardIdsFromOrder.reduce((acc: Task[], id: string) => {
let card = cards.find((c) => c.id === id);
if (card) {
acc.push(card);
}
return acc;
}, []);
if (currentColumn) {
// Maintain order of existing cards and append new ones
let existingCardIds = new Set(
currentColumn.cards.map((card: CardDef) => card.id),
);
let existingCards = currentColumn.cards.filter((card: CardDef) =>
cardsForStatus.some((c) => c.id === card.id),
);
let newCards = cardsForStatus.filter(
(card: CardDef) => !existingCardIds.has(card.id),
);
this.data.set(key, new DndColumn(key, [...newCards, ...existingCards]));
} else {
newCards = cards.filter((task) => task.status.label === statusLabel);
// First time loading this column
this.data.set(key, new DndColumn(key, cardsForStatus));
}
this.data.set(statusLabel, new DndColumn(statusLabel, newCards));
});
}

// Note:
// sourceColumnAfterDrag & targetColumnAfterDrag is the column state after the drag and drop
update(
draggedCard: DndItem,
_targetCard: DndItem | undefined,
sourceColumnAfterDrag: DndColumn,
targetColumnAfterDrag: DndColumn,
) {
let status = TaskStatusField.values.find(
(value) => value.label === targetColumnAfterDrag.title,
);
let cardInNewCol = targetColumnAfterDrag.cards.find(
(c: Task) => c.id === draggedCard.id,
);
if (cardInNewCol) {
cardInNewCol.status.label = status?.label;
cardInNewCol.status.index = status?.index;
}
//update the order of the cards in the column
this.order.set(
sourceColumnAfterDrag.title,
sourceColumnAfterDrag.cards.map((c: Task) => c.id),
);
this.order.set(
targetColumnAfterDrag.title,
targetColumnAfterDrag.cards.map((c: Task) => c.id),
);
return cardInNewCol;
}

get columns() {
return Array.from(this.data.values());
}

modify(_positional: never[], named: Args['named']) {
if (
named.query &&
(this.query === undefined || this.queryHasChanged(named.query))
) {
this.run.perform(named.query, named.realm);
}
this.hasColumnKey = named.hasColumnKey;
this.commit(named.cards, named.columnKeys);
}
}

export default function getTaskCardsResource(
parent: object,
query: () => Query | undefined,
realm: () => string,
cards: () => CardDef[],
columnKeys: () => string[],
hasColumnKey: () => (card: any, key: string) => boolean,
) {
return TaskCollection.from(parent, () => ({
named: {
realm: realm(),
query: query(),
cards: cards(),
columnKeys: columnKeys(),
hasColumnKey: hasColumnKey(),
},
}));
}
4 changes: 2 additions & 2 deletions packages/experiments-realm/productivity/task.gts
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,8 @@ class TaskIsolated extends Component<typeof Task> {
</div>

<div class='right-column'>
<div class='assignees'>
<h4>Assignees</h4>
<div class='assignee'>
<h4>Assignee</h4>

{{#if @model.assignee}}
<@fields.assignee
Expand Down
Loading
Loading