Vue Attract: Sort/filterable table based on Vue
Initial commit implementing sortable and filterable tables for attract using Vue.
This commit is contained in:
@@ -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.
|
||||
|
@@ -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')
|
||||
|
@@ -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.
|
||||
|
@@ -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')
|
||||
|
56
gulpfile.js
56
gulpfile.js
@@ -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.
|
||||
|
10
package.json
10
package.json
@@ -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",
|
||||
|
9
src/scripts/js/es6/common/api/assets.js
Normal file
9
src/scripts/js/es6/common/api/assets.js
Normal file
@@ -0,0 +1,9 @@
|
||||
function thenGetProjectAssets(projectId) {
|
||||
let where = {
|
||||
project: projectId,
|
||||
node_type: 'attract_asset'
|
||||
}
|
||||
return pillar.api.thenGetNodes(where);
|
||||
}
|
||||
|
||||
export { thenGetProjectAssets }
|
3
src/scripts/js/es6/common/api/init.js
Normal file
3
src/scripts/js/es6/common/api/init.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export {thenGetProjectAssets} from './assets'
|
||||
export {thenGetProjectShots} from './shots'
|
||||
export {thenGetTasks, thenGetProjectTasks} from './tasks'
|
9
src/scripts/js/es6/common/api/shots.js
Normal file
9
src/scripts/js/es6/common/api/shots.js
Normal file
@@ -0,0 +1,9 @@
|
||||
function thenGetProjectShots(projectId) {
|
||||
let where = {
|
||||
project: projectId,
|
||||
node_type: 'attract_shot'
|
||||
}
|
||||
return pillar.api.thenGetNodes(where);
|
||||
}
|
||||
|
||||
export { thenGetProjectShots }
|
20
src/scripts/js/es6/common/api/tasks.js
Normal file
20
src/scripts/js/es6/common/api/tasks.js
Normal 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 }
|
41
src/scripts/js/es6/common/auth/auth.js
Normal file
41
src/scripts/js/es6/common/auth/auth.js
Normal 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}
|
1
src/scripts/js/es6/common/auth/init.js
Normal file
1
src/scripts/js/es6/common/auth/init.js
Normal file
@@ -0,0 +1 @@
|
||||
export { AttractAuth } from './auth'
|
40
src/scripts/js/es6/common/vuecomponents/assetstable/Table.js
Normal file
40
src/scripts/js/es6/common/vuecomponents/assetstable/Table.js
Normal 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,
|
||||
}
|
||||
});
|
@@ -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 }
|
@@ -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 }
|
@@ -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 }
|
@@ -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 }
|
@@ -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 }
|
@@ -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 }
|
@@ -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 }
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 || '';
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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 }
|
@@ -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 }
|
@@ -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 }
|
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
3
src/scripts/js/es6/common/vuecomponents/init.js
Normal file
3
src/scripts/js/es6/common/vuecomponents/init.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import './assetstable/Table'
|
||||
import './taskstable/Table'
|
||||
import './shotstable/Table'
|
13
src/scripts/js/es6/common/vuecomponents/shotstable/Table.js
Normal file
13
src/scripts/js/es6/common/vuecomponents/shotstable/Table.js
Normal 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,
|
||||
},
|
||||
});
|
@@ -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 }
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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 }
|
@@ -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 }
|
@@ -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 }
|
40
src/scripts/js/es6/common/vuecomponents/taskstable/Table.js
Normal file
40
src/scripts/js/es6/common/vuecomponents/taskstable/Table.js
Normal 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,
|
||||
}
|
||||
});
|
@@ -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 }
|
@@ -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 }
|
@@ -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 }
|
@@ -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 }
|
@@ -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 }
|
@@ -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 }
|
@@ -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 }
|
@@ -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())) {
|
||||
|
188
src/styles/components/_attract_table.sass
Normal file
188
src/styles/components/_attract_table.sass
Normal 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
|
@@ -55,6 +55,9 @@
|
||||
@import _app_utils
|
||||
@import _app_base
|
||||
|
||||
// Attract components
|
||||
@import "components/attract_table"
|
||||
|
||||
@import _tasks
|
||||
@import _shots
|
||||
@import _dashboard
|
||||
|
@@ -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 %}
|
||||
|
@@ -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']) }}";
|
||||
|
@@ -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 %}
|
||||
|
@@ -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']) }}";
|
||||
|
@@ -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 %}
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user