Vue Attract: Sort/filterable table based on Vue

Initial commit implementing sortable and filterable tables for attract
using Vue.
This commit is contained in:
2019-02-12 09:08:37 +01:00
parent 66212ec5fa
commit 5e73720d91
51 changed files with 1375 additions and 485 deletions

View File

@@ -0,0 +1,9 @@
function thenGetProjectAssets(projectId) {
let where = {
project: projectId,
node_type: 'attract_asset'
}
return pillar.api.thenGetNodes(where);
}
export { thenGetProjectAssets }

View File

@@ -0,0 +1,3 @@
export {thenGetProjectAssets} from './assets'
export {thenGetProjectShots} from './shots'
export {thenGetTasks, thenGetProjectTasks} from './tasks'

View File

@@ -0,0 +1,9 @@
function thenGetProjectShots(projectId) {
let where = {
project: projectId,
node_type: 'attract_shot'
}
return pillar.api.thenGetNodes(where);
}
export { thenGetProjectShots }

View File

@@ -0,0 +1,20 @@
function thenGetTasks(parentId) {
let where = {
parent: parentId,
node_type: 'attract_task'
};
return pillar.api.thenGetNodes(where);
}
function thenGetProjectTasks(projectId) {
let where = {
project: projectId,
node_type: 'attract_task'
}
let embedded = {
parent: 1
}
return pillar.api.thenGetNodes(where, embedded);
}
export { thenGetTasks, thenGetProjectTasks }

View File

@@ -0,0 +1,41 @@
class ProjectAuth {
constructor() {
this.canCreateTask = false;
this.canCreateAsset = false;
}
}
class Auth {
constructor() {
this.perProjectAuth = {}
}
canUserCreateTask(projectId) {
let projectAuth = this.getProjectAuth(projectId);
return projectAuth.canCreateTask;
}
canUserCreateAsset(projectId) {
let projectAuth = this.getProjectAuth(projectId);
return projectAuth.canCreateAsset;
}
setUserCanCreateTask(projectId, canCreateTask) {
let projectAuth = this.getProjectAuth(projectId);
projectAuth.canCreateTask = canCreateTask;
}
setUserCanCreateAsset(projectId, canCreateAsset) {
let projectAuth = this.getProjectAuth(projectId);
projectAuth.canCreateAsset = canCreateAsset;
}
getProjectAuth(projectId) {
this.perProjectAuth[projectId] = this.perProjectAuth[projectId] || new ProjectAuth();
return this.perProjectAuth[projectId];
}
}
let AttractAuth = new Auth();
export {AttractAuth}

View File

@@ -0,0 +1 @@
export { AttractAuth } from './auth'

View File

@@ -0,0 +1,40 @@
let PillarTable = pillar.vuecomponents.table.PillarTable;
import {AssetColumnFactory} from './columns/AssetColumnFactory'
import {AssetRowsSource} from './rows/AssetRowsSource'
import {RowFilter} from '../attracttable/filter/RowFilter'
const TEMPLATE =`
<div class="pillar-table-actions">
<button class="action"
v-if="canAddAsset"
@click="createNewAsset"
>
<i class="pi-plus">New Asset</i>
</button>
</div>
`;
let TableActions = {
template: TEMPLATE,
computed: {
canAddAsset() {
let projectId = ProjectUtils.projectId();
return attract.auth.AttractAuth.canUserCreateAsset(projectId);
}
},
methods: {
createNewAsset() {
asset_create(ProjectUtils.projectUrl());
}
},
}
Vue.component('attract-assets-table', {
extends: PillarTable,
columnFactory: AssetColumnFactory,
rowsSource: AssetRowsSource,
components: {
'pillar-table-actions': TableActions,
'pillar-table-row-filter': RowFilter,
}
});

View File

@@ -0,0 +1,24 @@
import { TaskColumn } from '../../attracttable/columns/Tasks';
import { FirstTaskDueDate, NextTaskDueDate, LastTaskDueDate } from '../../attracttable/columns/TaskDueDate';
import { Status } from '../../attracttable/columns/Status';
import { RowObject } from '../../attracttable/columns/RowObject'
let ColumnFactoryBase = pillar.vuecomponents.table.columns.ColumnFactoryBase;
class AssetColumnFactory extends ColumnFactoryBase{
thenGetColumns() {
return this.thenGetProject()
.then((project) => {
let taskTypes = project.extension_props.attract.task_types.attract_asset;
let taskColumns = taskTypes.map((tType) => {
return new TaskColumn(tType, 'asset-task');
})
return [new Status(), new RowObject()]
.concat(taskColumns)
.concat([new NextTaskDueDate(),]);
})
}
}
export { AssetColumnFactory }

View File

@@ -0,0 +1,30 @@
import { AttractRowBase } from '../../attracttable/rows/AttractRowBase'
import { TaskEventListener } from '../../attracttable/rows/TaskEventListener';
class AssetRow extends AttractRowBase {
constructor(asset) {
super(asset);
this.tasks = [];
}
thenInit() {
return attract.api.thenGetTasks(this.getId())
.then((response) => {
this.tasks = response._items;
this.registerTaskEventListeners();
this.isInitialized = true;
});
}
registerTaskEventListeners() {
new TaskEventListener(this).register();
}
getTasksOfType(taskType) {
return this.tasks.filter((t) => {
return t.properties.task_type === taskType;
})
}
}
export { AssetRow }

View File

@@ -0,0 +1,18 @@
import { AttractRowsSourceBase } from '../../attracttable/rows/AttractRowsSourceBase'
import { AssetRow } from './AssetRow'
class AssetRowsSource extends AttractRowsSourceBase {
constructor(projectId) {
super(projectId, 'attract_asset', AssetRow);
}
thenInit() {
return attract.api.thenGetProjectAssets(this.projectId)
.then((result) => {
let assets = result._items;
this.initRowObjects(assets);
});
}
}
export { AssetRowsSource }

View File

@@ -0,0 +1,38 @@
let CellDefault = pillar.vuecomponents.table.cells.renderer.CellDefault;
const TEMPLATE =`
<div>
<a
@click.prevent="onClick()"
:href="cellLink"
>
{{ cellValue }}
</a>
</div>
`;
let CellRowObject = Vue.component('pillar-cell-row-object', {
extends: CellDefault,
template: TEMPLATE,
computed: {
cellLink() {
let project_url = ProjectUtils.projectUrl();
let item_type = this.itemType();
return `/attract/${project_url}/${item_type}s/${this.rowObject.getId()}`;
},
embededLink() {
return this.cellLink;
}
},
methods: {
onClick() {
item_open(this.rowObject.getId(), this.itemType(), true, ProjectUtils.projectUrl());
},
itemType() {
let node_type = this.rowObject.underlyingObject.node_type;
return node_type.replace('attract_', ''); // eg. attract_task to tasks
}
},
});
export { CellRowObject }

View File

@@ -0,0 +1,12 @@
let CellDefault = pillar.vuecomponents.table.cells.renderer.CellDefault;
let CellStatus = Vue.component('attract-cell-Status', {
extends: CellDefault,
computed: {
cellValue() {
return '';
},
},
});
export { CellStatus }

View File

@@ -0,0 +1,59 @@
let CellDefault = pillar.vuecomponents.table.cells.renderer.CellDefault;
const TEMPLATE =`
<div>
<div class="tasks">
<a
v-for="t in tasks"
:class="taskClass(t)"
:href="taskLink(t)"
:key="t._id"
:title="taskTitle(t)"
@click.prevent="onTaskClicked(t)"
/>
</div>
<button class="add-task-link"
v-if="canAddTask"
@click.prevent="onAddTask"
>
<i class="pi-plus">Task</i>
</button>
</div>
`;
let CellTasks = Vue.component('attract-cell-tasks', {
extends: CellDefault,
template: TEMPLATE,
computed: {
tasks() {
return this.rawCellValue;
},
canAddTask() {
let projectId = ProjectUtils.projectId();
return attract.auth.AttractAuth.canUserCreateTask(projectId);
}
},
methods: {
taskClass(task) {
return `task status-${task.properties.status}`
},
taskLink(task) {
let project_url = ProjectUtils.projectUrl();
let node_type = this.rowObject.underlyingObject.node_type;
let item_type = node_type.replace('attract_', '') + 's'; // eg. attract_asset to assets
return `/attract/${project_url}/${item_type}/with-task/${task._id}`;
},
taskTitle(task) {
let status = (task.properties.status || '').replace('_', ' ');
return `Task: ${task.name}\nStatus: ${status}`
},
onTaskClicked(task) {
task_open(task._id, ProjectUtils.projectUrl());
},
onAddTask(event) {
task_create(this.rowObject.getId(), this.column.taskType);
}
},
});
export { CellTasks }

View File

@@ -0,0 +1,19 @@
let ColumnBase = pillar.vuecomponents.table.columns.ColumnBase;
import { CellRowObject } from '../cells/renderer/CellRowObject'
class RowObject extends ColumnBase {
constructor() {
super('Name', 'row-object');
this.isMandatory = true;
}
getCellRenderer(rowObject) {
return CellRowObject.options.name;
}
getRawCellValue(rowObject) {
return rowObject.getName() || '<No Name>';
}
}
export { RowObject }

View File

@@ -0,0 +1,47 @@
import {CellStatus} from '../cells/renderer/CellStatus'
let ColumnBase = pillar.vuecomponents.table.columns.ColumnBase;
export class Status extends ColumnBase {
constructor() {
super('', 'attract-status');
this.isMandatory = true;
}
getCellRenderer(rowObject) {
return CellStatus.options.name;
}
getRawCellValue(rowObject) {
return rowObject.getProperties().status;
}
getCellTitle(rawCellValue, rowObject) {
function capitalize(str) {
if(str.length === 0) return str;
return str.charAt(0).toUpperCase() + str.slice(1);
}
let formatedStatus = capitalize(rawCellValue).replace('_', ' ');
return `Status: ${formatedStatus}`;
}
getCellClasses(rawCellValue, rowObject) {
let classes = super.getCellClasses(rawCellValue, rowObject);
classes[`status-${rawCellValue}`] = true;
return classes;
}
compareRows(rowObject1, rowObject2) {
let sortNbr1 = this.getSortNumber(rowObject1);
let sortNbr2 = this.getSortNumber(rowObject2);
if (sortNbr1 === sortNbr2) return 0;
return sortNbr1 < sortNbr2 ? -1 : 1;
}
getSortNumber(rowObject) {
let statusStr = rowObject.getProperties().status;
switch (statusStr) {
case 'on_hold': return 10;
case 'todo': return 20;
case 'in_progress': return 30;
case 'review': return 40;
case 'cbb': return 50;
case 'approved': return 60;
case 'final': return 70;
default: return 9999; // invalid status
}
}
}

View File

@@ -0,0 +1,100 @@
let ColumnBase = pillar.vuecomponents.table.columns.ColumnBase;
let CellPrettyDate = pillar.vuecomponents.table.cells.renderer.CellPrettyDate;
function firstDate(prevDate, task) {
let candidate = task.properties.due_date;
if (prevDate && candidate) {
if (prevDate !== candidate) {
return new Date(candidate) < new Date(prevDate) ? candidate : prevDate;
}
}
return prevDate || candidate;
}
function lastDate(prevDate, task) {
let candidate = task.properties.due_date;
if (prevDate && candidate) {
if (prevDate !== candidate) {
return new Date(candidate) > new Date(prevDate) ? candidate : prevDate;
}
}
return prevDate || candidate;
}
function nextDate(prevDate, task) {
let candidate = task.properties.due_date;
if(candidate && new Date(candidate) >= new Date()) {
return firstDate(prevDate, task);
}
return prevDate;
}
class DueDate extends ColumnBase {
getCellRenderer(rowObject) {
return CellPrettyDate.options.name;
}
getCellClasses(dueDate, rowObject) {
let classes = super.getCellClasses(dueDate, rowObject);
let isPostDueDate = false;
if (dueDate) {
isPostDueDate = new Date(dueDate) < new Date();
}
classes['warning'] = isPostDueDate;
return classes;
}
compareRows(rowObject1, rowObject2) {
let dueDateStr1 = this.getRawCellValue(rowObject1);
let dueDateStr2 = this.getRawCellValue(rowObject2);
if (dueDateStr1 === dueDateStr2) return 0;
if (dueDateStr1 && dueDateStr2) {
return new Date(dueDateStr1) < new Date(dueDateStr2) ? -1 : 1;
}
return dueDateStr1 ? -1 : 1;
}
getCellTitle(rawCellValue, rowObject) {
return rawCellValue;
}
}
export class FirstTaskDueDate extends DueDate {
constructor() {
super('First Due Date', 'first-duedate');
}
getRawCellValue(rowObject) {
let tasks = rowObject.tasks || [];
return tasks.reduce(firstDate, undefined) || '';
}
}
export class LastTaskDueDate extends DueDate {
constructor() {
super('Last Due Date', 'last-duedate');
}
getRawCellValue(rowObject) {
let tasks = rowObject.tasks || [];
return tasks.reduce(lastDate, undefined) || '';
}
}
export class NextTaskDueDate extends DueDate {
constructor() {
super('Next Due Date', 'next-duedate');
}
getRawCellValue(rowObject) {
let tasks = rowObject.tasks || [];
return tasks.reduce(nextDate, undefined) || '';
}
}
export class TaskDueDate extends DueDate {
constructor() {
super('Due Date', 'duedate');
}
getRawCellValue(rowObject) {
let task = rowObject.getTask();
return task.properties.due_date || '';
}
}

View File

@@ -0,0 +1,24 @@
let ColumnBase = pillar.vuecomponents.table.columns.ColumnBase;
import { CellTasks } from '../cells/renderer/CellTasks'
export class TaskColumn extends ColumnBase {
constructor(taskType, columnType) {
super(taskType, columnType);
this.taskType = taskType;
}
getCellRenderer(rowObject) {
return CellTasks.options.name;
}
getRawCellValue(rowObject) {
return rowObject.getTasksOfType(this.taskType);
}
compareRows(rowObject1, rowObject2) {
let numTasks1 = this.getRawCellValue(rowObject1).length;
let numTasks2 = this.getRawCellValue(rowObject2).length;
if (numTasks1 === numTasks2) return 0;
return numTasks1 < numTasks2 ? -1 : 1;
}
}

View File

@@ -0,0 +1,98 @@
let RowFilterBase = pillar.vuecomponents.table.filter.RowFilter;
const TEMPLATE =`
<div class="pillar-table-row-filter">
<input
placeholder="Filter by name"
v-model="nameQuery"
/>
<pillar-dropdown>
<i class="pi-filter"
slot="button"
title="Row filter"
/>
<ul class="settings-menu"
slot="menu"
>
<li>
Status:
</li>
<li>
<input type="checkbox"
v-model="showAssetStatus.final"
/> Final
</li>
<li>
<input type="checkbox"
v-model="showAssetStatus.approved"
/> Approved
</li>
<li>
<input type="checkbox"
v-model="showAssetStatus.cbb"
/> Cbb
</li>
<li>
<input type="checkbox"
v-model="showAssetStatus.review"
/> Review
</li>
<li>
<input type="checkbox"
v-model="showAssetStatus.in_progress"
/> In Progress
</li>
<li>
<input type="checkbox"
v-model="showAssetStatus.todo"
/> Todo
</li>
<li>
<input type="checkbox"
v-model="showAssetStatus.on_hold"
/> On Hold
</li>
</ul>
</pillar-dropdown>
</div>
`;
let RowFilter = {
extends: RowFilterBase,
template: TEMPLATE,
props: {
rowObjects: Array
},
data() {
return {
showAssetStatus: {
todo: true,
in_progress: true,
on_hold: true,
approved: true,
cbb: true,
final: true,
review: true,
},
}
},
computed: {
nameQueryLoweCase() {
return this.nameQuery.toLowerCase();
},
visibleRowObjects() {
return this.rowObjects.filter((row) => {
if (!this.hasShowStatus(row)) return false;
return this.filterByName(row);
});
}
},
methods: {
hasShowStatus(rowObject) {
let status = rowObject.getProperties().status;
return !(this.showAssetStatus[status] === false); // To handle invalid statuses
},
},
};
export { RowFilter }

View File

@@ -0,0 +1,14 @@
let RowBase = pillar.vuecomponents.table.rows.RowBase;
class AttractRowBase extends RowBase {
constructor(underlyingObject) {
super(underlyingObject);
pillar.events.Nodes.onUpdated(this.getId(), this.onRowUpdated.bind(this));
}
onRowUpdated(event, updatedObj) {
this.underlyingObject = updatedObj;
}
}
export { AttractRowBase }

View File

@@ -0,0 +1,38 @@
let RowObjectsSourceBase = pillar.vuecomponents.table.rows.RowObjectsSourceBase;
class AttractRowsSourceBase extends RowObjectsSourceBase {
constructor(projectId, node_type, rowClass) {
super(projectId);
this.node_type = node_type;
this.rowClass = rowClass;
}
createRow(node) {
let row = new this.rowClass(node);
row.thenInit();
this.registerListeners(row);
return row;
}
initRowObjects(nodes) {
this.rowObjects = nodes.map(this.createRow.bind(this));
pillar.events.Nodes.onCreated(this.node_type, this.onNodeCreated.bind(this));
}
registerListeners(rowObject) {
pillar.events.Nodes.onDeleted(rowObject.getId(), this.onNodeDeleted.bind(this));
}
onNodeDeleted(event, nodeId) {
this.rowObjects = this.rowObjects.filter((rowObj) => {
return rowObj.getId() !== nodeId;
});
}
onNodeCreated(event, node) {
let rowObj = this.createRow(node);
this.rowObjects = this.rowObjects.concat(rowObj);
}
}
export { AttractRowsSourceBase }

View File

@@ -0,0 +1,37 @@
/**
* Helper class that listens to events triggered when a RowObject task is updated/created/deleted and keep the tasks
* array of a RowObject up to date accordingly.
*/
export class TaskEventListener {
constructor(rowWithTasks) {
this.rowObject = rowWithTasks;
}
register() {
pillar.events.Nodes.onParentCreated(this.rowObject.getId(), 'attract_task', this.onTaskCreated.bind(this));
this.rowObject.tasks.forEach(this.registerEventListeners.bind(this));
}
registerEventListeners(task) {
pillar.events.Nodes.onUpdated(task._id, this.onTaskUpdated.bind(this));
pillar.events.Nodes.onDeleted(task._id, this.onTaskDeleted.bind(this));
}
onTaskCreated(event, newTask) {
this.registerEventListeners(newTask);
this.rowObject.tasks = this.rowObject.tasks.concat(newTask);
}
onTaskUpdated(event, updatedTask) {
this.rowObject.tasks = this.rowObject.tasks.map((t) => {
return t._id === updatedTask._id ? updatedTask : t;
});
}
onTaskDeleted(event, taskId) {
this.rowObject.tasks = this.rowObject.tasks.filter((t) => {
return t._id !== taskId;
});
}
}

View File

@@ -0,0 +1,3 @@
import './assetstable/Table'
import './taskstable/Table'
import './shotstable/Table'

View File

@@ -0,0 +1,13 @@
let PillarTable = pillar.vuecomponents.table.PillarTable;
import {ShotsColumnFactory} from './columns/ShotsColumnFactory'
import {ShotRowsSource} from './rows/ShotRowsSource'
import {RowFilter} from '../attracttable/filter/RowFilter'
Vue.component('attract-shots-table', {
extends: PillarTable,
columnFactory: ShotsColumnFactory,
rowsSource: ShotRowsSource,
components: {
'pillar-table-row-filter': RowFilter,
},
});

View File

@@ -0,0 +1,63 @@
let CellDefault = pillar.vuecomponents.table.cells.renderer.CellDefault;
const TEMPLATE =`
<div>
<img
v-if="img.src"
:src="img.src"
:alt="img.alt"
:height="img.height"
:width="img.width"
/>
<generic-placeholder
v-if="isLoading"
/>
</div>
`;
let CellPicture = Vue.component('pillar-cell-picture', {
extends: CellDefault,
template: TEMPLATE,
data() {
return {
img: {},
failed: false,
}
},
computed: {
isLoading() {
if(!this.failed) {
return !!this.rawCellValue && !this.img.src;
}
return false;
}
},
created() {
if (this.rawCellValue) {
this.loadThumbnail(this.rawCellValue);
}
},
watch: {
rawCellValue(newValue) {
this.loadThumbnail(newValue);
}
},
methods: {
loadThumbnail(imgId) {
this.img = {};
pillar.utils.thenLoadImage(imgId, 't')
.then(fileDoc => {
this.img = {
src: fileDoc.link,
alt: fileDoc.name,
width: fileDoc.width,
height: fileDoc.height,
}
}).fail(() => {
this.failed = true;
});
}
},
});
export { CellPicture }

View File

@@ -0,0 +1,15 @@
import {CellPicture} from '../cells/renderer/Picture'
let ColumnBase = pillar.vuecomponents.table.columns.ColumnBase;
export class Picture extends ColumnBase {
constructor() {
super('Thumbnail', 'thumbnail');
this.isSortable = false;
}
getCellRenderer(rowObject) {
return CellPicture.options.name;
}
getRawCellValue(rowObject) {
return rowObject.underlyingObject.picture;
}
}

View File

@@ -0,0 +1,25 @@
import { TaskColumn } from '../../attracttable/columns/Tasks';
import { FirstTaskDueDate, NextTaskDueDate, LastTaskDueDate } from '../../attracttable/columns/TaskDueDate';
import { Status } from '../../attracttable/columns/Status';
import { Picture } from '../columns/Picture'
import { RowObject } from '../../attracttable/columns/RowObject'
let ColumnFactoryBase = pillar.vuecomponents.table.columns.ColumnFactoryBase;
class ShotsColumnFactory extends ColumnFactoryBase{
thenGetColumns() {
return this.thenGetProject()
.then((project) => {
let taskTypes = project.extension_props.attract.task_types.attract_shot;
let taskColumns = taskTypes.map((tType) => {
return new TaskColumn(tType, 'shot-task');
})
return [new Status(), new Picture(), new RowObject()]
.concat(taskColumns)
.concat([new NextTaskDueDate(),]);
})
}
}
export { ShotsColumnFactory }

View File

@@ -0,0 +1,38 @@
import {AttractRowBase} from '../../attracttable/rows/AttractRowBase'
import { TaskEventListener } from '../../attracttable/rows/TaskEventListener';
class ShotRow extends AttractRowBase {
constructor(shot) {
super(shot);
this.tasks = [];
}
thenInit() {
return attract.api.thenGetTasks(this.getId())
.then((response) => {
this.tasks = response._items;
this.registerTaskEventListeners();
this.isInitialized = true;
})
}
registerTaskEventListeners() {
new TaskEventListener(this).register();
}
getTasksOfType(taskType) {
return this.tasks.filter((t) => {
return t.properties.task_type === taskType;
})
}
getRowClasses() {
let classes = super.getRowClasses()
if(this.isInitialized) {
classes['shot-not-in-edit'] = !this.underlyingObject.properties.used_in_edit;
}
return classes;
}
}
export { ShotRow }

View File

@@ -0,0 +1,18 @@
import { AttractRowsSourceBase } from '../../attracttable/rows/AttractRowsSourceBase'
import { ShotRow } from './ShotRow'
class ShotRowsSource extends AttractRowsSourceBase {
constructor(projectId) {
super(projectId, 'attract_asset', ShotRow);
}
thenInit() {
return attract.api.thenGetProjectShots(this.projectId)
.then((result) => {
let shots = result._items;
this.initRowObjects(shots);
});
}
}
export { ShotRowsSource }

View File

@@ -0,0 +1,40 @@
let PillarTable = pillar.vuecomponents.table.PillarTable;
import {TasksColumnFactory} from './columns/TasksColumnFactory'
import {TaskRowsSource} from './rows/TaskRowsSource'
import {RowFilter} from '../attracttable/filter/RowFilter'
const TEMPLATE =`
<div class="pillar-table-actions">
<button class="action"
v-if="canAddTask"
@click="createNewTask"
>
<i class="pi-plus">New Task</i>
</button>
</div>
`;
let TableActions = {
template: TEMPLATE,
computed: {
canAddTask() {
let projectId = ProjectUtils.projectId();
return attract.auth.AttractAuth.canUserCreateTask(projectId);
}
},
methods: {
createNewTask() {
task_create(undefined, 'generic');
}
},
}
Vue.component('attract-tasks-table', {
extends: PillarTable,
columnFactory: TasksColumnFactory,
rowsSource: TaskRowsSource,
components: {
'pillar-table-actions': TableActions,
'pillar-table-row-filter': RowFilter,
}
});

View File

@@ -0,0 +1,42 @@
let CellDefault = pillar.vuecomponents.table.cells.renderer.CellDefault;
const TEMPLATE =`
<div>
<a
v-if="rawCellValue"
@click.prevent="onClick()"
:href="cellLink"
>
{{ cellValue }}
</a>
</div>
`;
let ParentNameCell = Vue.component('pillar-cell-parent-name', {
extends: CellDefault,
template: TEMPLATE,
computed: {
cellTitle() {
return this.rawCellValue;
},
cellLink() {
let project_url = ProjectUtils.projectUrl();
let item_type = this.itemType();
return `/attract/${project_url}/${item_type}s/${this.rowObject.getParent()._id}`;
},
embededLink() {
return this.cellLink;
}
},
methods: {
onClick() {
item_open(this.rowObject.getParent()._id, this.itemType(), false, ProjectUtils.projectUrl());
},
itemType() {
let node_type = this.rowObject.getParent().node_type;
return node_type.replace('attract_', ''); // eg. attract_task to tasks
}
},
});
export { ParentNameCell }

View File

@@ -0,0 +1,30 @@
let ColumnBase = pillar.vuecomponents.table.columns.ColumnBase;
import {ParentNameCell} from '../cells/ParentName'
class ParentName extends ColumnBase {
constructor() {
super('Parent', 'parent-name');
}
getCellRenderer(rowObject) {
return ParentNameCell.options.name;
}
getRawCellValue(rowObject) {
if(!rowObject.getParent()) return '';
return rowObject.getParent().name || '<No Name>';
}
compareRows(rowObject1, rowObject2) {
let parent1 = rowObject1.getParent();
let parent2 = rowObject2.getParent();
if (parent1 && parent2) {
if (parent1.name === parent2.name) {
return parent1._id < parent2._id ? -1 : 1;
}
}
return super.compareRows(rowObject1, rowObject2);
}
}
export { ParentName }

View File

@@ -0,0 +1,13 @@
let ColumnBase = pillar.vuecomponents.table.columns.ColumnBase;
class ShortCode extends ColumnBase {
constructor() {
super('Short Code', 'short-code');
}
getRawCellValue(rowObject) {
return rowObject.getTask().properties.shortcode || '';
}
}
export { ShortCode }

View File

@@ -0,0 +1,13 @@
let ColumnBase = pillar.vuecomponents.table.columns.ColumnBase;
class TaskType extends ColumnBase {
constructor() {
super('Type', 'task-type');
}
getRawCellValue(rowObject) {
return rowObject.getTask().properties.task_type || '';
}
}
export { TaskType }

View File

@@ -0,0 +1,27 @@
import { Status } from '../../attracttable/columns/Status'
import { RowObject } from '../../attracttable/columns/RowObject'
import { TaskDueDate } from '../../attracttable/columns/TaskDueDate'
import { TaskType } from './TaskType'
import { ShortCode } from './ShortCode'
import { ParentName } from './ParentName'
let ColumnFactoryBase = pillar.vuecomponents.table.columns.ColumnFactoryBase;
class TasksColumnFactory extends ColumnFactoryBase{
thenGetColumns() {
return this.thenGetProject()
.then((project) => {
return [
new Status(),
new ParentName(),
new RowObject(),
new ShortCode(),
new TaskType(),
new TaskDueDate(),
];
})
}
}
export { TasksColumnFactory }

View File

@@ -0,0 +1,29 @@
import {AttractRowBase} from '../../attracttable/rows/AttractRowBase'
class TaskRow extends AttractRowBase {
constructor(task) {
super(task);
this.parent = undefined;
if (task.parent) {
// Deattach parent from task to avoid parent to be overwritten when task is updated
let parentId = task.parent._id;
this.parent = task.parent;
task.parent = parentId;
pillar.events.Nodes.onUpdated(parentId, this.onParentUpdated.bind(this));
}
}
getTask() {
return this.underlyingObject;
}
getParent() {
return this.parent;
}
onParentUpdated(event, updatedObj) {
this.parent = updatedObj;
}
}
export { TaskRow }

View File

@@ -0,0 +1,18 @@
import { AttractRowsSourceBase } from '../../attracttable/rows/AttractRowsSourceBase'
import { TaskRow } from './TaskRow'
class TaskRowsSource extends AttractRowsSourceBase {
constructor(projectId) {
super(projectId, 'attract_task', TaskRow);
}
thenInit() {
return attract.api.thenGetProjectTasks(this.projectId)
.then((result) => {
let tasks = result._items;
this.initRowObjects(tasks);
});
}
}
export { TaskRowsSource }