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

@@ -110,7 +110,7 @@ class ShotAssetManager(object):
node = pillarsdk.Node(node_props)
node.create(api=api)
return node
return pillarsdk.Node.find(node._id, api=api)
def create_shot(self, project):
"""Creates a new shot, owned by the current user.

View File

@@ -24,25 +24,14 @@ log = logging.getLogger(__name__)
@perproject_blueprint.route('/with-task/<task_id>', endpoint='with_task')
@attract_project_view(extension_props=True)
def for_project(project, attract_props, task_id=None, asset_id=None):
node_type_name = node_type_asset['name']
assets, tasks_for_assets, task_types_for_template = routes_common.for_project(
node_type_name,
attract_props['task_types'][node_type_name],
project, attract_props, task_id, asset_id)
can_create = current_attract.auth.current_user_may(current_attract.auth.Actions.USE)
navigation_links = project_navigation_links(project, pillar_api())
extension_sidebar_links = current_app.extension_sidebar_links(project)
return render_template('attract/assets/for_project.html',
assets=assets,
tasks_for_assets=tasks_for_assets,
task_types=task_types_for_template,
open_task_id=task_id,
open_asset_id=asset_id,
project=project,
attract_props=attract_props,
can_create_task=can_create,
can_create_asset=can_create,
navigation_links=navigation_links,
@@ -94,7 +83,7 @@ def create_asset(project):
project_url=project['url'],
asset_id=asset['_id'])
resp.status_code = 201
return flask.make_response(flask.jsonify({'asset_id': asset['_id']}), 201)
return flask.make_response(flask.jsonify(asset.to_dict()), 201)
@perproject_blueprint.route('/<asset_id>/activities')

View File

@@ -49,7 +49,7 @@ class TaskManager(object):
task = pillarsdk.Node(node_props)
task.create(api=api)
return task
return pillarsdk.Node.find(task._id, api=api)
def edit_task(self, task_id, **fields):
"""Edits a task.

View File

@@ -178,7 +178,7 @@ def create_task(project):
task_id=task['_id'])
resp.status_code = 201
return flask.make_response(flask.jsonify({'task_id': task['_id']}), 201)
return flask.make_response(flask.jsonify(task.to_dict()), 201)
@perproject_blueprint.route('/<task_id>/activities')

View File

@@ -13,6 +13,13 @@ var rename = require('gulp-rename');
var sass = require('gulp-sass');
var sourcemaps = require('gulp-sourcemaps');
var uglify = require('gulp-uglify-es').default;
var browserify = require('browserify');
var babelify = require('babelify');
var sourceStream = require('vinyl-source-stream');
var glob = require('glob');
var es = require('event-stream');
var path = require('path');
var buffer = require('vinyl-buffer');
var enabled = {
chmod: argv.production,
@@ -73,11 +80,51 @@ gulp.task('scripts', function() {
.pipe(gulpif(enabled.liveReload, livereload()));
});
function browserify_base(entry) {
let pathSplited = path.dirname(entry).split(path.sep);
let moduleName = pathSplited[pathSplited.length - 1];
return browserify({
entries: [entry],
standalone: 'attract.' + moduleName,
})
.transform(babelify, { "presets": ["@babel/preset-env"] })
.bundle()
.pipe(gulpif(enabled.failCheck, plumber()))
.pipe(sourceStream(path.basename(entry)))
.pipe(buffer())
.pipe(rename({
basename: moduleName,
extname: '.min.js'
}));
}
function browserify_common() {
return glob.sync('src/scripts/js/es6/common/**/init.js').map(browserify_base);
}
gulp.task('scripts_browserify', function(done) {
glob('src/scripts/js/es6/individual/**/init.js', function(err, files) {
if(err) done(err);
var tasks = files.map(function(entry) {
return browserify_base(entry)
.pipe(gulpif(enabled.maps, sourcemaps.init()))
.pipe(gulpif(enabled.uglify, uglify()))
.pipe(gulpif(enabled.maps, sourcemaps.write(".")))
.pipe(gulp.dest(destination.js));
});
es.merge(tasks).on('end', done);
})
});
/* Collection of scripts in src/scripts/tutti/ to merge into tutti.min.js */
/* Since it's always loaded, it's only for functions that we want site-wide */
gulp.task('scripts_tutti', function() {
gulp.src('src/scripts/tutti/**/*.js')
gulp.task('scripts_tutti', function(done) {
let toUglify = ['src/scripts/tutti/**/*.js']
es.merge(gulp.src(toUglify), ...browserify_common())
.pipe(gulpif(enabled.failCheck, plumber()))
.pipe(gulpif(enabled.maps, sourcemaps.init()))
.pipe(concat("tutti.min.js"))
@@ -86,11 +133,12 @@ gulp.task('scripts_tutti', function() {
.pipe(gulpif(enabled.chmod, chmod(0o644)))
.pipe(gulp.dest(destination.js))
.pipe(gulpif(enabled.liveReload, livereload()));
done();
});
// While developing, run 'gulp watch'
gulp.task('watch',function() {
gulp.task('watch',function(done) {
// Only listen for live reloads if ran with --livereload
if (argv.livereload){
livereload.listen();
@@ -100,6 +148,8 @@ gulp.task('watch',function() {
gulp.watch('src/templates/**/*.pug',['templates']);
gulp.watch('src/scripts/*.js',['scripts']);
gulp.watch('src/scripts/tutti/*.js',['scripts_tutti']);
gulp.watch('src/scripts/js/**/*.js', ['scripts_browserify', 'scripts_tutti']);
done();
});
// Erases all generated files in output directories.

View File

@@ -7,6 +7,12 @@
"url": "git://git.blender.org/attract.git"
},
"devDependencies": {
"@babel/core": "7.1.6",
"@babel/preset-env": "7.1.6",
"acorn": "5.7.3",
"babel-core": "7.0.0-bridge.0",
"babelify": "10.0.0",
"browserify": "16.2.3",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^6.0.0",
"gulp-cached": "^1.1.1",
@@ -21,7 +27,9 @@
"gulp-sass": "^4.0.1",
"gulp-sourcemaps": "^2.6.4",
"gulp-uglify-es": "^1.0.4",
"minimist": "^1.2.0"
"minimist": "^1.2.0",
"vinyl-buffer": "1.0.1",
"vinyl-source-stream": "2.0.0"
},
"dependencies": {
"bootstrap": "^4.1.3",

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 }

View File

@@ -1,28 +1,3 @@
/**
* Removes the task from the task list and shot list, and show the 'task-add-link'
* when this was the last task in its category.
*/
function _remove_task_from_list(task_id) {
var $task_link = $('#task-' + task_id)
var $task_link_parent = $task_link.parent();
$task_link.hideAndRemove(300, function() {
if ($task_link_parent.children('.task-link').length == 0) {
$task_link_parent.find('.task-add-link').removeClass('hidden');
}
});
}
/**
* Removes the 'active' class from any element whose ID starts with
* shot-, asset-, or task-.
*/
function deactivateItemLinks()
{
$('[id^="shot-"]').removeClass('active');
$('[id^="asset-"]').removeClass('active');
$('[id^="task-"]').removeClass('active');
}
/**
* Open an item such as tasks/shots in the #item-details div
*/
@@ -39,19 +14,9 @@ function item_open(item_id, item_type, pushState, project_url)
}
}
// Style elements starting with item_type and dash, e.g. "#shot-uuid"
deactivateItemLinks();
var current_item = $('#' + item_type + '-' + item_id);
current_item.addClass('processing');
// Special case to highlight the shot row when opening task in shot or asset context
var pu_ctx = ProjectUtils.context();
var pc_ctx_shot_asset = (pu_ctx == 'shot' || pu_ctx == 'asset');
if (pc_ctx_shot_asset && item_type == 'task'){
$('[id^="shot-"]').removeClass('active');
$('[id^="asset-"]').removeClass('active');
$('#task-' + item_id).closest('.table-row').addClass('active');
}
var item_url = '/attract/' + project_url + '/' + item_type + 's/' + item_id;
var push_url = item_url;
@@ -60,22 +25,14 @@ function item_open(item_id, item_type, pushState, project_url)
}
item_url += '?context=' + pu_ctx;
statusBarSet('default', 'Loading ' + item_type + '…');
$.get(item_url, function(item_data) {
statusBarClear();
$('#item-details').html(item_data);
$('#col_right .col_header span.header_text').text(item_type + ' details');
current_item
.removeClass('processing newborn')
.addClass('active');
}).fail(function(xhr) {
if (console) {
console.log('Error fetching task', item_id, 'from', item_url);
console.log('XHR:', xhr);
current_item
.removeClass('processing')
}
toastr.error('Failed to open ' + item_type);
@@ -141,7 +98,9 @@ function asset_create(project_url)
};
$.post(url, data, function(asset_data) {
window.location.href = asset_data.asset_id;
/* window.location.href = asset_data.asset_id; */
pillar.events.Nodes.triggerCreated(asset_data);
asset_open(asset_data._id);
})
.fail(function(xhr) {
if (console) {
@@ -152,58 +111,6 @@ function asset_create(project_url)
});
}
/**
* Adds the task item to the shots/tasks list.
*
* 'shot_id' can be undefined if the task isn't attached to a shot.
*/
function task_add(shot_id, task_id, task_type)
{
if (task_id === undefined || task_type === undefined) {
throw new ReferenceError("task_add(" + shot_id + ", " + task_id + ", " + task_type + ") called.");
}
var project_url = ProjectUtils.projectUrl();
var url = '/attract/' + project_url + '/tasks/' + task_id;
var context = ProjectUtils.context();
if (context == 'task') {
/* WARNING: This is a copy of an element of attract/tasks/for_project .item-list.col-list
* If that changes, change this too. */
$('.item-list.task').append('\
<a class="col-list-item task-list-item status-todo task-link active"\
href="' + url + '"\
data-task-id="' + task_id + '"\
id="task-' + task_id + '">\
<span class="status-indicator"></span>\
<span class="name">-save your task first-</span>\
<span class="due_date">-</span>\
</a>\
');
} else if (context == 'shot' || context == 'asset') {
if (shot_id === undefined) {
throw new ReferenceError("task_add(" + shot_id + ", " + task_id + ", " + task_type + ") called in " + context + " context.");
}
var $list_cell = $('#' + context + '-' + shot_id + ' .table-cell.task-type.' + task_type);
var url = '/attract/' + project_url + '/' + context + 's/with-task/' + task_id;
/* WARNING: This is a copy of an element of attract/shots/for_project .item-list.col-list
* If that changes, change this too. */
$list_cell.append('\
<a class="status-todo task-link active newborn"\
title="-save your task first-"\
href="' + url + '"\
data-task-id="' + task_id + '"\
id="task-' + task_id + '">\
</a>\
');
$list_cell.find('.task-add.task-add-link').addClass('hidden');
} else {
if (console) console.log('task_add: not doing much in context', context);
}
}
/**
* Create a task and show it in the #item-details div.
@@ -228,8 +135,8 @@ function task_create(shot_id, task_type)
$.post(url, data, function(task_data) {
if (console) console.log('Task created:', task_data);
task_open(task_data.task_id);
task_add(shot_id, task_data.task_id, task_type);
pillar.events.Nodes.triggerCreated(task_data);
task_open(task_data._id);
})
.fail(function(xhr) {
if (console) {
@@ -257,18 +164,15 @@ function attract_form_save(form_id, item_id, item_save_url, options)
var $item = $('#' + item_id);
$button.attr('disabled', true);
$item.addClass('processing');
statusBarSet('', 'Saving ' + options.type + '…');
if (console) console.log('Sending:', payload);
$.post(item_save_url, payload)
.done(function(saved_item) {
if (console) console.log('Done saving', saved_item);
toastr.success('Saved ' + options.type + '. ' + saved_item._updated);
statusBarSet('success', 'Saved ' + options.type + '. ' + saved_item._updated, 'pi-check');
pillar.events.Nodes.triggerUpdated(saved_item);
$form.find("input[name='_etag']").val(saved_item._etag);
if (options.done) options.done($item, saved_item);
@@ -279,13 +183,12 @@ function attract_form_save(form_id, item_id, item_save_url, options)
$button.removeClass('btn-outline-success').addClass('btn-danger');
statusBarSet('error', 'Failed saving. ' + xhr_or_response_data.status, 'pi-warning');
toastr.error('Failed saving. ' + xhr_or_response_data.status);
if (options.fail) options.fail($item, xhr_or_response_data);
})
.always(function() {
$button.attr('disabled', false);
$item.removeClass('processing');
if (options.always) options.always($item);
})
@@ -297,21 +200,6 @@ function attract_form_save(form_id, item_id, item_save_url, options)
function task_save(task_id, task_url) {
return attract_form_save('item_form', 'task-' + task_id, task_url, {
done: function($task, saved_task) {
// Update the task list.
// NOTE: this is tightly linked to the HTML of the task list in for_project.jade.
$('.task-name-' + saved_task._id).text(saved_task.name).flashOnce();
$task.find('span.name').text(saved_task.name);
$task.find('span.status').text(saved_task.properties.status.replace('_', ' '));
if (saved_task.properties.due_date){
$task.find('span.due_date').text(moment().to(saved_task.properties.due_date));
}
$task
.removeClassPrefix('status-')
.addClass('status-' + saved_task.properties.status)
.flashOnce()
;
task_open(task_id);
},
fail: function($item, xhr_or_response_data) {
@@ -329,13 +217,6 @@ function task_save(task_id, task_url) {
function shot_save(shot_id, shot_url) {
return attract_form_save('item_form', 'shot-' + shot_id, shot_url, {
done: function($shot, saved_shot) {
// Update the shot list.
$('.shot-name-' + saved_shot._id).text(saved_shot.name);
$shot
.removeClassPrefix('status-')
.addClass('status-' + saved_shot.properties.status)
.flashOnce()
;
shot_open(shot_id);
},
fail: function($item, xhr_or_response_data) {
@@ -353,19 +234,6 @@ function shot_save(shot_id, shot_url) {
function asset_save(asset_id, asset_url) {
return attract_form_save('item_form', 'asset-' + asset_id, asset_url, {
done: function($asset, saved_asset) {
// Update the asset list.
// NOTE: this is tightly linked to the HTML of the asset list in for_project.jade.
$('.item-name-' + saved_asset._id).text(saved_asset.name).flashOnce();
$asset.find('span.name').text(saved_asset.name);
$asset.find('span.due_date').text(moment().to(saved_asset.properties.due_date));
$asset.find('span.status').text(saved_asset.properties.status.replace('_', ' '));
$asset
.removeClassPrefix('status-')
.addClass('status-' + saved_asset.properties.status)
.flashOnce()
;
asset_open(asset_id);
},
fail: function($item, xhr_or_response_data) {
@@ -384,9 +252,6 @@ function task_delete(task_id, task_etag, task_delete_url) {
if (task_id === undefined || task_etag === undefined || task_delete_url === undefined) {
throw new ReferenceError("task_delete(" + task_id + ", " + task_etag + ", " + task_delete_url + ") called.");
}
$('#task-' + task_id).addClass('processing');
$.ajax({
type: 'DELETE',
url: task_delete_url,
@@ -395,13 +260,12 @@ function task_delete(task_id, task_etag, task_delete_url) {
.done(function(e) {
if (console) console.log('Task', task_id, 'was deleted.');
$('#item-details').fadeOutAndClear();
_remove_task_from_list(task_id);
pillar.events.Nodes.triggerDeleted(task_id);
statusBarSet('success', 'Task deleted successfully', 'pi-check');
toastr.success('Task deleted');
})
.fail(function(xhr) {
statusBarSet('error', 'Unable to delete task, code ' + xhr.status, 'pi-warning');
toastr.error('Unable to delete task, code ' + xhr.status);
if (xhr.status == 412) {
alert('Someone else edited this task before you deleted it; refresh to try again.');
@@ -429,7 +293,7 @@ function loadActivities(url)
console.log('XHR:', xhr);
}
statusBarSet('error', 'Opening activity log failed.', 'pi-warning');
toastr.error('Opening activity log failed.');
if (xhr.status) {
$('#activities').html(xhr.responseText);
@@ -441,31 +305,6 @@ function loadActivities(url)
});
}
$(function() {
$("a.shot-link[data-shot-id]").click(function(e) {
e.preventDefault();
// delegateTarget is the thing the event hander was attached to,
// rather than the thing we clicked on.
var shot_id = e.delegateTarget.dataset.shotId;
shot_open(shot_id);
});
$("a.asset-link[data-asset-id]").click(function(e) {
e.preventDefault();
// delegateTarget is the thing the event hander was attached to,
// rather than the thing we clicked on.
var asset_id = e.delegateTarget.dataset.assetId;
asset_open(asset_id);
});
$("a.task-link[data-task-id]").click(function(e) {
e.preventDefault();
var task_id = e.delegateTarget.dataset.taskId;
var project_url = e.delegateTarget.dataset.projectUrl; // fine if undefined
task_open(task_id, project_url);
});
});
var save_on_ctrl_enter = ['shot', 'asset', 'task'];
$(document).on('keyup', function(e){
if ($.inArray(save_on_ctrl_enter, ProjectUtils.context())) {

View File

@@ -0,0 +1,188 @@
$thumbnail-max-width: 110px
$thumbnail-max-height: calc(110px * (9/16))
.pillar-table-container
background-color: white
height: 100%
.pillar-table
display: flex
flex-direction: column
width: 100%
height: 95% // TODO: Investigate why some rows are outside screen if 100%
.pillar-table-head
display: flex
flex-direction: row
position: relative
box-shadow: 0 5 $color-background-dark
.cell-content
display: flex
flex-direction: row
align-items: center
height: 100%
font-size: .9em
.column-sort
display: flex
opacity: 0
flex-direction: column
.sort-action
&:hover
background-color: $color-background-active
&:hover
.column-sort
opacity: 1
.pillar-table-row-group
display: block
overflow-y: auto
height: 100%
.pillar-table-row:nth-child(odd)
background-color: $color-background-dark
.pillar-table-row
display: flex
flex-direction: row
transition: all 250ms ease-in-out
background-color: $color-background-light
&:hover
background-color: $color-background-active
&.is-busy
+stripes-animate
+stripes(transparent, rgba($color-background-active, .6), -45deg, 4em)
animation-duration: 4s
.pillar-table-container.attract-tasks-table
.pillar-table-row
min-height: 1.4em
.pillar-table-container.attract-shots-table
.pillar-table-row
min-height: $thumbnail-max-height
&.shot-not-in-edit
+stripes(transparent, rgba($color-warning, .2), -45deg, 4em)
.pillar-table-container.attract-assets-table
.pillar-table-row
min-height: 2.6em
.pillar-cell
display: flex
flex-direction: column
flex-grow: 1
flex-basis: 0
overflow: hidden
white-space: nowrap
text-overflow: ellipsis
justify-content: center
&.highlight
background-color: rgba($color-background-active, .4)
&.warning
background-color: rgba($color-warning, .4)
&.header-cell
text-transform: capitalize
color: $color-text-dark-secondary
&.attract-status
flex: 0
flex-basis: 1em
&.thumbnail
flex: 0
flex-basis: $thumbnail-max-width
text-align: center
img
max-width: $thumbnail-max-width
height: auto
&.task-type
text-transform: capitalize
a
overflow: hidden
text-overflow: ellipsis
@include status-color-property(background-color, '', '')
.add-task-link
opacity: 0
cursor: pointer
vertical-align: middle
color: $color-primary
border: none
background: none
width: max-content
padding: 0
&:hover
text-decoration-line: underline
&:hover
.add-task-link
opacity: 1
.tasks
display: flex
.task
@include status-color-property(background-color, '', '')
width: 1em
height: 1em
border-radius: 1em
&:hover
box-shadow: inset 0px 0px 5px $color-background-active-dark
.pillar-table-menu
display: flex
flex-direction: row
.settings-menu
display: flex
flex-direction: column
position: absolute
background-color: white
list-style: none
margin: 0
padding: 0
text-transform: capitalize
z-index: $z-index-base + 1
box-shadow: 0 2px 5px rgba(black, .4)
.pillar-table-row-filter
display: flex
flex-direction: row
.pillar-table-actions
margin-left: auto
.action
cursor: pointer
vertical-align: middle
color: $color-primary
border: none
background: none
&:hover
text-decoration-line: underline
.pillar-table-column-filter
margin-left: auto
.settings-menu
right: 0em
.pillar-table-row-item
display: inline-block
.pillar-table-row-enter, .pillar-table-row-leave-to
opacity: 0

View File

@@ -55,6 +55,9 @@
@import _app_utils
@import _app_base
// Attract components
@import "components/attract_table"
@import _tasks
@import _shots
@import _dashboard

View File

@@ -3,77 +3,9 @@
| {% block page_title %}Assets - {{ project.name }}{% endblock %}
| {% block attractbody %}
#col_main
.col_header.item-list-header
a.item-project(href="{{url_for('projects.view', project_url=project.url)}}") {{ project.name }}
span.item-extra Assets ({{ assets | count }})
| {% if can_create_asset %}
a#item-add(href="javascript:asset_create('{{ project.url }}');") + Create Asset
| {% endif %}
.item-list.asset.col-scrollable
.table
.table-head.is-fixed
.table-head.original
.table-row
.table-cell.asset-status
| {# disabled until users can actually upload a thumbnail
.table-cell.item-thumbnail
span.collapser.thumbnails(title="Collapse thumbnails") Thumbnail
| #}
.table-cell.item-name
span.collapser(title="Collapse name column") Name
| {% for task_type in task_types %}
.table-cell.task-type(class="{{ task_type }}")
span.collapser(title="Collapse {{ task_type or 'Other' }} column") {{ task_type or 'other' }}
| {% endfor %}
.table-body
| {% for asset in assets %}
.table-row(
id="asset-{{ asset._id }}",
class="status-{{ asset.properties.status }} {{ asset.properties.used_in_edit | yesno(' ,not-in-edit, ') }}")
.table-cell.item-status(
title="Status: {{ asset.properties.status | undertitle }}")
| {# disabled until users can actually upload a thumbnail
.table-cell.item-thumbnail
a(
data-asset-id="{{ asset._id }}",
href="{{ url_for('attract.assets.perproject.view_asset', project_url=project.url, asset_id=asset._id) }}",
class="status-{{ asset.properties.status }} asset-link")
img(src="{{ asset._thumbnail }}",
alt="Thumbnail",
style='width: 110px; height: {{ asset._thumbnail_height }}')
| #}
.table-cell.item-name
a(
data-asset-id="{{ asset._id }}",
href="{{ url_for('attract.assets.perproject.view_asset', project_url=project.url, asset_id=asset._id) }}",
class="status-{{ asset.properties.status }} asset-link")
span(class="item-name-{{ asset._id }}") {{ asset.name }}
| {% for task_type in task_types %}
.table-cell.task-type(class="{{ task_type }}")
| {% for task in tasks_for_assets[asset._id][task_type] %}
a(
data-task-id="{{ task._id }}",
id="task-{{ task._id }}",
href="{{ url_for('attract.assets.perproject.with_task', project_url=project.url, task_id=task._id) }}",
class="status-{{ task.properties.status }} task-link",
title="{{ task.properties.status | undertitle }} task: {{ task.name }}")
| {# First letter of the status. Disabled until we provide the user setting to turn it off
span {{ task.properties.status[0] }}
| #}
| {% endfor %}
| {% if can_create_task %}
button.task-add(
title="Add a new '{{ task_type }}' task",
class="task-add-link {% if tasks_for_assets[asset._id][task_type] %}hidden{% endif %}",
data-parent-id='{{ asset._id }}',
data-task-type='{{ task_type }}')
i.pi-plus
| Task
| {% endif %}
| {% endfor %}
| {% endfor %}
attract-assets-table#table(
project-id="{{ project._id}}"
)
.col-splitter
@@ -83,10 +15,19 @@
#item-details.col-scrollable
.item-details-empty
| Select an Asset or Task
| {% endblock %}
| {% block footer_scripts %}
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/clipboard.min.js')}}")
script(src="{{ url_for('static_attract', filename='assets/js/vendor/jquery-resizable-0.20.min.js')}}")
script.
{% if can_create_task %}
attract.auth.AttractAuth.setUserCanCreateTask('{{project._id}}', true);
{% endif %}
{% if can_create_asset %}
attract.auth.AttractAuth.setUserCanCreateAsset('{{project._id}}', true);
{% endif %}
{% if open_task_id %}
$(function() { item_open('{{ open_task_id }}', 'task', false); });
{% endif %}
@@ -94,73 +35,10 @@ script.
$(function() { item_open('{{ open_asset_id }}', 'asset', false); });
{% endif %}
$('button.task-add').on('click', function(e){
e.preventDefault();
var parent_id = $(this).attr('data-parent-id');
var task_type = $(this).attr('data-task-type');
task_create(parent_id, task_type);
});
var same_cells;
/* Collapse columns by clicking on the title */
$('.table-head .table-cell span.collapser').on('click', function(e){
e.stopPropagation();
/* We need to find every cell matching the same classes */
same_cells = '.' + $(this).parent().attr('class').split(' ').join('.');
$(same_cells).hide();
/* Add the spacer which we later click to expand */
$('<div class="table-cell-spacer ' + $(this).text() + '" title="Expand ' + $(this).text() + '"></div>').insertAfter(same_cells);
});
$('body').on('click', '.table-cell-spacer', function(){
/* We need to find every cell matching the same classes */
same_cells = '.' + $(this).prev().attr('class').split(' ').join('.');
$(same_cells).show();
$(same_cells).next().remove();
});
$('.table-body .table-cell').mouseenter(function(){
same_cells = '.' + $(this).attr('class').split(' ').join('.');
$('.table-head ' + same_cells).addClass('highlight');
}).mouseleave(function(){
same_cells = '.' + $(this).attr('class').split(' ').join('.');
$('.table-head ' + same_cells).removeClass('highlight');
});
$('.table-head .table-cell').mouseenter(function(){
same_cells = '.' + $(this).attr('class').split(' ').join('.');
$('.table-body ' + same_cells).addClass('highlight');
}).mouseleave(function(){
same_cells = '.' + $(this).attr('class').split(' ').join('.');
$('.table-body ' + same_cells).removeClass('highlight');
});
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/clipboard.min.js')}}")
script(src="{{ url_for('static_attract', filename='assets/js/vendor/jquery-resizable-0.20.min.js')}}")
script.
// The fixed table header situation, check out 00_utils.js
cloneChildren('.item-list .table-head .table-row', '.item-list .table-head.is-fixed');
scrollHeaderHorizontal('.item-list', '.item-list .table-head.is-fixed', 50);
// For every column, set the width of the fixed header using the original columns width
setHeaderCellsWidth('.item-list .table-head.original .table-row', '.item-list .table-head.is-fixed .table-row');
new Vue({el:'#table'})
$("#col_main").resizable({
handleSelector: ".col-splitter",
resizeHeight: false,
onDrag: function () {
setHeaderCellsWidth('.item-list .table-head.original .table-row', '.item-list .table-head.is-fixed .table-row');
}
});
// Set height of asset-list and item details so we can scroll inside them
$(window).on('load resize', function(){
var window_height = $(window).height() - 50; // header is 50px
$('#shot-list').css({'height': window_height});
$('#item-details').css({'height': window_height});
});
| {% endblock footer_scripts %}

View File

@@ -92,7 +92,7 @@ script.
var clipboard = new Clipboard('.copy-to-clipboard');
clipboard.on('success', function(e) {
statusBarSet('info', 'Copied asset ID to clipboard', 'pi-check');
toastr.info('Copied asset ID to clipboard')
});
var activities_url = "{{ url_for('.activities', project_url=project.url, asset_id=asset['_id']) }}";

View File

@@ -3,71 +3,9 @@
| {% block page_title %}Shots - {{ project.name }}{% endblock %}
| {% block attractbody %}
#col_main
.col_header.item-list-header
a.item-project(href="{{url_for('projects.view', project_url=project.url)}}") {{ project.name }}
span.item-extra(title='In the edit') {{ stats.nr_of_shots }} shots | {{ stats.total_frame_count }} frames
.item-list.shot.col-scrollable
.table
.table-head.is-fixed
.table-head.original
.table-row
.table-cell.item-status
.table-cell.item-thumbnail
span.collapser.thumbnails(title="Collapse thumbnails") Thumbnail
.table-cell.item-name
span.collapser(title="Collapse name column") Name
| {% for task_type in task_types %}
.table-cell.task-type(class="{{ task_type }}")
span.collapser(title="Collapse {{ task_type or 'Other' }} column") {{ task_type or 'other' }}
| {% endfor %}
.table-body
| {% for shot in shots %}
.table-row(
id="shot-{{ shot._id }}",
class="status-{{ shot.properties.status }} {{ shot.properties.used_in_edit | yesno(' ,not-in-edit, ') }}")
.table-cell.item-status(
title="Status: {{ shot.properties.status | undertitle }}")
.table-cell.item-thumbnail
a(
data-shot-id="{{ shot._id }}",
href="{{ url_for('attract.shots.perproject.view_shot', project_url=project.url, shot_id=shot._id) }}",
class="status-{{ shot.properties.status }} shot-link")
img(src="{{ shot._thumbnail }}",
alt="Thumbnail",
style='width: 110px; height: {{ shot._thumbnail_height }}')
.table-cell.item-name
a(
data-shot-id="{{ shot._id }}",
href="{{ url_for('attract.shots.perproject.view_shot', project_url=project.url, shot_id=shot._id) }}",
class="status-{{ shot.properties.status }} shot-link")
span(class="item-name-{{ shot._id }}") {{ shot.name }}
| {% for task_type in task_types %}
.table-cell.task-type(class="{{ task_type }}")
| {% for task in tasks_for_shots[shot._id][task_type] %}
a(
data-task-id="{{ task._id }}",
id="task-{{ task._id }}",
href="{{ url_for('attract.shots.perproject.with_task', project_url=project.url, task_id=task._id) }}",
class="status-{{ task.properties.status }} task-link",
title="{{ task.properties.status | undertitle }} task: {{ task.name }}")
| {# First letter of the status. Disabled until we provide the user setting to turn it off
span {{ task.properties.status[0] }}
| #}
| {% endfor %}
| {% if can_create_task %}
button.task-add(
title="Add a new '{{ task_type }}' task",
class="task-add-link {% if tasks_for_shots[shot._id][task_type] %}hidden{% endif %}"
data-parent-id='{{ shot._id }}',
data-task-type='{{ task_type }}')
i.pi-plus
| Task
| {% endif %}
| {% endfor %}
| {% endfor %}
attract-shots-table#table(
project-id="{{ project._id}}"
)
.col-splitter
@@ -77,13 +15,16 @@
#item-details.col-scrollable
.item-details-empty
| Select a Shot or Task
| {% endblock %}
| {% block footer_scripts %}
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/clipboard.min.js')}}")
script(src="{{ url_for('static_attract', filename='assets/js/vendor/jquery-resizable-0.20.min.js')}}")
script.
script.
{% if can_create_task %}
attract.auth.AttractAuth.setUserCanCreateTask('{{project._id}}', true);
{% endif %}
// Open task or shot on load
{% if open_task_id %}
$(function() { item_open('{{ open_task_id }}', 'task', false); });
@@ -92,70 +33,10 @@ script.
$(function() { item_open('{{ open_shot_id }}', 'shot', false); });
{% endif %}
// Create tasks
$('button.task-add').on('click', function(e){
e.preventDefault();
var parent_id = $(this).attr('data-parent-id');
var task_type = $(this).attr('data-task-type');
task_create(parent_id, task_type);
});
/* Collapse columns by clicking on the title */
var same_cells;
$('.table-head .table-cell span.collapser').on('click', function(e){
e.stopPropagation();
/* We need to find every cell matching the same classes */
same_cells = '.' + $(this).parent().attr('class').split(' ').join('.');
$(same_cells).hide();
/* Add the spacer which we later click to expand */
$('<div class="table-cell-spacer ' + $(this).text() + '" title="Expand ' + $(this).text() + '"></div>').insertAfter(same_cells);
// For every column, set the width of the fixed header using the original columns width
setHeaderCellsWidth('.item-list .table-head.original .table-row', '.item-list .table-head.is-fixed .table-row');
});
$('body').on('click', '.table-cell-spacer', function(){
/* We need to find every cell matching the same classes */
same_cells = '.' + $(this).prev().attr('class').split(' ').join('.');
$(same_cells).show();
$(same_cells).next().remove();
});
$('.table-body .table-cell').mouseenter(function(){
same_cells = '.' + $(this).attr('class').split(' ').join('.');
$('.table-head ' + same_cells).addClass('highlight');
}).mouseleave(function(){
same_cells = '.' + $(this).attr('class').split(' ').join('.');
$('.table-head ' + same_cells).removeClass('highlight');
});
$('.table-head .table-cell').mouseenter(function(){
same_cells = '.' + $(this).attr('class').split(' ').join('.');
$('.table-body ' + same_cells).addClass('highlight');
}).mouseleave(function(){
same_cells = '.' + $(this).attr('class').split(' ').join('.');
$('.table-body ' + same_cells).removeClass('highlight');
});
// The fixed table header situation, check out 00_utils.js
cloneChildren('.item-list .table-head .table-row', '.item-list .table-head.is-fixed');
scrollHeaderHorizontal('.item-list', '.item-list .table-head.is-fixed', 50);
// For every column, set the width of the fixed header using the original columns width
setHeaderCellsWidth('.item-list .table-head.original .table-row', '.item-list .table-head.is-fixed .table-row');
script.
new Vue({el:'#table'})
$("#col_main").resizable({
handleSelector: ".col-splitter",
resizeHeight: false,
onDrag: function () {
setHeaderCellsWidth('.item-list .table-head.original .table-row', '.item-list .table-head.is-fixed .table-row');
}
});
| {% endblock footer_scripts %}

View File

@@ -124,7 +124,7 @@ script.
var clipboard = new Clipboard('.copy-to-clipboard');
clipboard.on('success', function(e) {
statusBarSet('info', 'Copied shot ID to clipboard', 'pi-check');
toastr.info('Copied shot ID to clipboard')
});
var activities_url = "{{ url_for('.activities', project_url=project.url, shot_id=shot['_id']) }}";

View File

@@ -4,27 +4,9 @@
| {% block attractbody %}
#col_main
.col_header.item-list-header
a.item-project(href="{{url_for('projects.view', project_url=project.url)}}") {{ project.name }}
span.item-extra Tasks ({{ tasks | count }})
| {% if can_create_task %}
a#item-add(href="javascript:task_create(undefined, 'generic');") + Create Task
| {% endif %}
.item-list.task.col-list.col-scrollable
| {% for task in tasks %}
//- NOTE: this is tightly linked to the JS in tasks.js, function task_add()
a.col-list-item.task-list-item(
id="task-{{task._id}}",
data-task-id="{{task._id}}",
class="status-{{ task.properties.status }} task-link",
href="{{ url_for('attract.tasks.perproject.view_task', project_url=project.url, task_id=task._id) }}")
span.status-indicator(title="Status: {{ task.properties.status | undertitle }}")
| {% if task._parent_info %}
span.shotname(title="Shot {{ task._parent_info.name }}") {{ task._parent_info.name }}
| {% endif %}
span.name {{ task.name }}
span.due_date {{ task.properties.due_date | pretty_date | hide_none }}
| {% endfor %}
attract-tasks-table#table(
project-id="{{ project._id}}"
)
.col-splitter
@@ -41,13 +23,17 @@ script.
{% if open_task_id %}
$(function() { item_open('{{ open_task_id }}', 'task', false); });
{% endif %}
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/clipboard.min.js')}}")
script(src="{{ url_for('static_attract', filename='assets/js/vendor/jquery-resizable-0.20.min.js')}}")
script.
{% if can_create_task %}
attract.auth.AttractAuth.setUserCanCreateTask('{{project._id}}', true);
{% endif %}
new Vue({el:'#table'})
$("#col_main").resizable({
handleSelector: ".col-splitter",
resizeHeight: false
resizeHeight: false,
});
| {% endblock %}
| {% endblock footer_scripts %}

View File

@@ -195,12 +195,12 @@ script.
new Clipboard('.copy-to-clipboard-id')
.on('success', function(e) {
statusBarSet('info', 'Copied task ID to clipboard', 'pi-check');
toastr.info('Copied task ID to clipboard');
});
new Clipboard('.copy-to-clipboard-shortcode')
.on('success', function(e) {
statusBarSet('info', 'Copied task shortcode to clipboard', 'pi-check');
toastr.info('Copied task shortcode to clipboard');
});
loadActivities("{{ url_for('.activities', project_url=project.url, task_id=task['_id']) }}"); // from 10_tasks.js