From 5e73720d9161efb7357be6698db33d5bd88dff03 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Tue, 12 Feb 2019 09:08:37 +0100 Subject: [PATCH] Vue Attract: Sort/filterable table based on Vue Initial commit implementing sortable and filterable tables for attract using Vue. --- attract/shots_and_assets/__init__.py | 2 +- attract/shots_and_assets/routes_assets.py | 13 +- attract/tasks/__init__.py | 2 +- attract/tasks/routes.py | 2 +- gulpfile.js | 56 +++++- package.json | 10 +- src/scripts/js/es6/common/api/assets.js | 9 + src/scripts/js/es6/common/api/init.js | 3 + src/scripts/js/es6/common/api/shots.js | 9 + src/scripts/js/es6/common/api/tasks.js | 20 ++ src/scripts/js/es6/common/auth/auth.js | 41 ++++ src/scripts/js/es6/common/auth/init.js | 1 + .../common/vuecomponents/assetstable/Table.js | 40 ++++ .../assetstable/columns/AssetColumnFactory.js | 24 +++ .../assetstable/rows/AssetRow.js | 30 +++ .../assetstable/rows/AssetRowsSource.js | 18 ++ .../cells/renderer/CellRowObject.js | 38 ++++ .../attracttable/cells/renderer/CellStatus.js | 12 ++ .../attracttable/cells/renderer/CellTasks.js | 59 ++++++ .../attracttable/columns/RowObject.js | 19 ++ .../attracttable/columns/Status.js | 47 +++++ .../attracttable/columns/TaskDueDate.js | 100 ++++++++++ .../attracttable/columns/Tasks.js | 24 +++ .../attracttable/filter/RowFilter.js | 98 +++++++++ .../attracttable/rows/AttractRowBase.js | 14 ++ .../rows/AttractRowsSourceBase.js | 38 ++++ .../attracttable/rows/TaskEventListener.js | 37 ++++ .../js/es6/common/vuecomponents/init.js | 3 + .../common/vuecomponents/shotstable/Table.js | 13 ++ .../shotstable/cells/renderer/Picture.js | 63 ++++++ .../shotstable/columns/Picture.js | 15 ++ .../shotstable/columns/ShotsColumnFactory.js | 25 +++ .../vuecomponents/shotstable/rows/ShotRow.js | 38 ++++ .../shotstable/rows/ShotRowsSource.js | 18 ++ .../common/vuecomponents/taskstable/Table.js | 40 ++++ .../taskstable/cells/ParentName.js | 42 ++++ .../taskstable/columns/ParentName.js | 30 +++ .../taskstable/columns/ShortCode.js | 13 ++ .../taskstable/columns/TaskType.js | 13 ++ .../taskstable/columns/TasksColumnFactory.js | 27 +++ .../vuecomponents/taskstable/rows/TaskRow.js | 29 +++ .../taskstable/rows/TaskRowsSource.js | 18 ++ src/scripts/tutti/10_tasks.js | 185 ++--------------- src/styles/components/_attract_table.sass | 188 ++++++++++++++++++ src/styles/main.sass | 3 + src/templates/attract/assets/for_project.pug | 150 ++------------ .../attract/assets/view_asset_embed.pug | 2 +- src/templates/attract/shots/for_project.pug | 139 +------------ .../attract/shots/view_shot_embed.pug | 2 +- src/templates/attract/tasks/for_project.pug | 34 +--- .../attract/tasks/view_task_embed.pug | 4 +- 51 files changed, 1375 insertions(+), 485 deletions(-) create mode 100644 src/scripts/js/es6/common/api/assets.js create mode 100644 src/scripts/js/es6/common/api/init.js create mode 100644 src/scripts/js/es6/common/api/shots.js create mode 100644 src/scripts/js/es6/common/api/tasks.js create mode 100644 src/scripts/js/es6/common/auth/auth.js create mode 100644 src/scripts/js/es6/common/auth/init.js create mode 100644 src/scripts/js/es6/common/vuecomponents/assetstable/Table.js create mode 100644 src/scripts/js/es6/common/vuecomponents/assetstable/columns/AssetColumnFactory.js create mode 100644 src/scripts/js/es6/common/vuecomponents/assetstable/rows/AssetRow.js create mode 100644 src/scripts/js/es6/common/vuecomponents/assetstable/rows/AssetRowsSource.js create mode 100644 src/scripts/js/es6/common/vuecomponents/attracttable/cells/renderer/CellRowObject.js create mode 100644 src/scripts/js/es6/common/vuecomponents/attracttable/cells/renderer/CellStatus.js create mode 100644 src/scripts/js/es6/common/vuecomponents/attracttable/cells/renderer/CellTasks.js create mode 100644 src/scripts/js/es6/common/vuecomponents/attracttable/columns/RowObject.js create mode 100644 src/scripts/js/es6/common/vuecomponents/attracttable/columns/Status.js create mode 100644 src/scripts/js/es6/common/vuecomponents/attracttable/columns/TaskDueDate.js create mode 100644 src/scripts/js/es6/common/vuecomponents/attracttable/columns/Tasks.js create mode 100644 src/scripts/js/es6/common/vuecomponents/attracttable/filter/RowFilter.js create mode 100644 src/scripts/js/es6/common/vuecomponents/attracttable/rows/AttractRowBase.js create mode 100644 src/scripts/js/es6/common/vuecomponents/attracttable/rows/AttractRowsSourceBase.js create mode 100644 src/scripts/js/es6/common/vuecomponents/attracttable/rows/TaskEventListener.js create mode 100644 src/scripts/js/es6/common/vuecomponents/init.js create mode 100644 src/scripts/js/es6/common/vuecomponents/shotstable/Table.js create mode 100644 src/scripts/js/es6/common/vuecomponents/shotstable/cells/renderer/Picture.js create mode 100644 src/scripts/js/es6/common/vuecomponents/shotstable/columns/Picture.js create mode 100644 src/scripts/js/es6/common/vuecomponents/shotstable/columns/ShotsColumnFactory.js create mode 100644 src/scripts/js/es6/common/vuecomponents/shotstable/rows/ShotRow.js create mode 100644 src/scripts/js/es6/common/vuecomponents/shotstable/rows/ShotRowsSource.js create mode 100644 src/scripts/js/es6/common/vuecomponents/taskstable/Table.js create mode 100644 src/scripts/js/es6/common/vuecomponents/taskstable/cells/ParentName.js create mode 100644 src/scripts/js/es6/common/vuecomponents/taskstable/columns/ParentName.js create mode 100644 src/scripts/js/es6/common/vuecomponents/taskstable/columns/ShortCode.js create mode 100644 src/scripts/js/es6/common/vuecomponents/taskstable/columns/TaskType.js create mode 100644 src/scripts/js/es6/common/vuecomponents/taskstable/columns/TasksColumnFactory.js create mode 100644 src/scripts/js/es6/common/vuecomponents/taskstable/rows/TaskRow.js create mode 100644 src/scripts/js/es6/common/vuecomponents/taskstable/rows/TaskRowsSource.js create mode 100644 src/styles/components/_attract_table.sass diff --git a/attract/shots_and_assets/__init__.py b/attract/shots_and_assets/__init__.py index e71d3f1..a0018a4 100644 --- a/attract/shots_and_assets/__init__.py +++ b/attract/shots_and_assets/__init__.py @@ -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. diff --git a/attract/shots_and_assets/routes_assets.py b/attract/shots_and_assets/routes_assets.py index 7b9b033..6c92334 100644 --- a/attract/shots_and_assets/routes_assets.py +++ b/attract/shots_and_assets/routes_assets.py @@ -24,25 +24,14 @@ log = logging.getLogger(__name__) @perproject_blueprint.route('/with-task/', 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('//activities') diff --git a/attract/tasks/__init__.py b/attract/tasks/__init__.py index 6610a55..518db1f 100644 --- a/attract/tasks/__init__.py +++ b/attract/tasks/__init__.py @@ -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. diff --git a/attract/tasks/routes.py b/attract/tasks/routes.py index fe1d2d1..79c2e58 100644 --- a/attract/tasks/routes.py +++ b/attract/tasks/routes.py @@ -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('//activities') diff --git a/gulpfile.js b/gulpfile.js index 424e94b..edc624f 100644 --- a/gulpfile.js +++ b/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. diff --git a/package.json b/package.json index b68a308..7eb6877 100644 --- a/package.json +++ b/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", diff --git a/src/scripts/js/es6/common/api/assets.js b/src/scripts/js/es6/common/api/assets.js new file mode 100644 index 0000000..1ff4010 --- /dev/null +++ b/src/scripts/js/es6/common/api/assets.js @@ -0,0 +1,9 @@ +function thenGetProjectAssets(projectId) { + let where = { + project: projectId, + node_type: 'attract_asset' + } + return pillar.api.thenGetNodes(where); +} + +export { thenGetProjectAssets } diff --git a/src/scripts/js/es6/common/api/init.js b/src/scripts/js/es6/common/api/init.js new file mode 100644 index 0000000..8786408 --- /dev/null +++ b/src/scripts/js/es6/common/api/init.js @@ -0,0 +1,3 @@ +export {thenGetProjectAssets} from './assets' +export {thenGetProjectShots} from './shots' +export {thenGetTasks, thenGetProjectTasks} from './tasks' diff --git a/src/scripts/js/es6/common/api/shots.js b/src/scripts/js/es6/common/api/shots.js new file mode 100644 index 0000000..c3f1d9c --- /dev/null +++ b/src/scripts/js/es6/common/api/shots.js @@ -0,0 +1,9 @@ +function thenGetProjectShots(projectId) { + let where = { + project: projectId, + node_type: 'attract_shot' + } + return pillar.api.thenGetNodes(where); +} + +export { thenGetProjectShots } diff --git a/src/scripts/js/es6/common/api/tasks.js b/src/scripts/js/es6/common/api/tasks.js new file mode 100644 index 0000000..739d9ce --- /dev/null +++ b/src/scripts/js/es6/common/api/tasks.js @@ -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 } diff --git a/src/scripts/js/es6/common/auth/auth.js b/src/scripts/js/es6/common/auth/auth.js new file mode 100644 index 0000000..fd04ac4 --- /dev/null +++ b/src/scripts/js/es6/common/auth/auth.js @@ -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} diff --git a/src/scripts/js/es6/common/auth/init.js b/src/scripts/js/es6/common/auth/init.js new file mode 100644 index 0000000..1f5b1fc --- /dev/null +++ b/src/scripts/js/es6/common/auth/init.js @@ -0,0 +1 @@ +export { AttractAuth } from './auth' diff --git a/src/scripts/js/es6/common/vuecomponents/assetstable/Table.js b/src/scripts/js/es6/common/vuecomponents/assetstable/Table.js new file mode 100644 index 0000000..cee5073 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/assetstable/Table.js @@ -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 =` +
+ +
+`; + +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, + } +}); diff --git a/src/scripts/js/es6/common/vuecomponents/assetstable/columns/AssetColumnFactory.js b/src/scripts/js/es6/common/vuecomponents/assetstable/columns/AssetColumnFactory.js new file mode 100644 index 0000000..e614187 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/assetstable/columns/AssetColumnFactory.js @@ -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 } diff --git a/src/scripts/js/es6/common/vuecomponents/assetstable/rows/AssetRow.js b/src/scripts/js/es6/common/vuecomponents/assetstable/rows/AssetRow.js new file mode 100644 index 0000000..cf64bc1 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/assetstable/rows/AssetRow.js @@ -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 } diff --git a/src/scripts/js/es6/common/vuecomponents/assetstable/rows/AssetRowsSource.js b/src/scripts/js/es6/common/vuecomponents/assetstable/rows/AssetRowsSource.js new file mode 100644 index 0000000..53f109b --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/assetstable/rows/AssetRowsSource.js @@ -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 } diff --git a/src/scripts/js/es6/common/vuecomponents/attracttable/cells/renderer/CellRowObject.js b/src/scripts/js/es6/common/vuecomponents/attracttable/cells/renderer/CellRowObject.js new file mode 100644 index 0000000..c0b4537 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/attracttable/cells/renderer/CellRowObject.js @@ -0,0 +1,38 @@ +let CellDefault = pillar.vuecomponents.table.cells.renderer.CellDefault; + +const TEMPLATE =` +
+ + {{ cellValue }} + +
+`; + +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 } diff --git a/src/scripts/js/es6/common/vuecomponents/attracttable/cells/renderer/CellStatus.js b/src/scripts/js/es6/common/vuecomponents/attracttable/cells/renderer/CellStatus.js new file mode 100644 index 0000000..92eaa9c --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/attracttable/cells/renderer/CellStatus.js @@ -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 } diff --git a/src/scripts/js/es6/common/vuecomponents/attracttable/cells/renderer/CellTasks.js b/src/scripts/js/es6/common/vuecomponents/attracttable/cells/renderer/CellTasks.js new file mode 100644 index 0000000..6966cfe --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/attracttable/cells/renderer/CellTasks.js @@ -0,0 +1,59 @@ +let CellDefault = pillar.vuecomponents.table.cells.renderer.CellDefault; + +const TEMPLATE =` +
+
+ +
+ +
+`; + +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 } diff --git a/src/scripts/js/es6/common/vuecomponents/attracttable/columns/RowObject.js b/src/scripts/js/es6/common/vuecomponents/attracttable/columns/RowObject.js new file mode 100644 index 0000000..63e80f2 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/attracttable/columns/RowObject.js @@ -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() || ''; + } +} + +export { RowObject } diff --git a/src/scripts/js/es6/common/vuecomponents/attracttable/columns/Status.js b/src/scripts/js/es6/common/vuecomponents/attracttable/columns/Status.js new file mode 100644 index 0000000..754d364 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/attracttable/columns/Status.js @@ -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 + } + } +} diff --git a/src/scripts/js/es6/common/vuecomponents/attracttable/columns/TaskDueDate.js b/src/scripts/js/es6/common/vuecomponents/attracttable/columns/TaskDueDate.js new file mode 100644 index 0000000..c56bd06 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/attracttable/columns/TaskDueDate.js @@ -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 || ''; + } +} diff --git a/src/scripts/js/es6/common/vuecomponents/attracttable/columns/Tasks.js b/src/scripts/js/es6/common/vuecomponents/attracttable/columns/Tasks.js new file mode 100644 index 0000000..d7521ec --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/attracttable/columns/Tasks.js @@ -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; + } +} diff --git a/src/scripts/js/es6/common/vuecomponents/attracttable/filter/RowFilter.js b/src/scripts/js/es6/common/vuecomponents/attracttable/filter/RowFilter.js new file mode 100644 index 0000000..eeeb095 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/attracttable/filter/RowFilter.js @@ -0,0 +1,98 @@ +let RowFilterBase = pillar.vuecomponents.table.filter.RowFilter; +const TEMPLATE =` +
+ + + + +
    +
  • + Status: +
  • +
  • + Final +
  • +
  • + Approved +
  • +
  • + Cbb +
  • +
  • + Review +
  • +
  • + In Progress +
  • +
  • + Todo +
  • +
  • + On Hold +
  • +
+
+
+`; + +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 } diff --git a/src/scripts/js/es6/common/vuecomponents/attracttable/rows/AttractRowBase.js b/src/scripts/js/es6/common/vuecomponents/attracttable/rows/AttractRowBase.js new file mode 100644 index 0000000..02ea156 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/attracttable/rows/AttractRowBase.js @@ -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 } diff --git a/src/scripts/js/es6/common/vuecomponents/attracttable/rows/AttractRowsSourceBase.js b/src/scripts/js/es6/common/vuecomponents/attracttable/rows/AttractRowsSourceBase.js new file mode 100644 index 0000000..315003e --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/attracttable/rows/AttractRowsSourceBase.js @@ -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 } diff --git a/src/scripts/js/es6/common/vuecomponents/attracttable/rows/TaskEventListener.js b/src/scripts/js/es6/common/vuecomponents/attracttable/rows/TaskEventListener.js new file mode 100644 index 0000000..24b0cd3 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/attracttable/rows/TaskEventListener.js @@ -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; + }); + } +} diff --git a/src/scripts/js/es6/common/vuecomponents/init.js b/src/scripts/js/es6/common/vuecomponents/init.js new file mode 100644 index 0000000..ba9914a --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/init.js @@ -0,0 +1,3 @@ +import './assetstable/Table' +import './taskstable/Table' +import './shotstable/Table' diff --git a/src/scripts/js/es6/common/vuecomponents/shotstable/Table.js b/src/scripts/js/es6/common/vuecomponents/shotstable/Table.js new file mode 100644 index 0000000..2c8e6c6 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/shotstable/Table.js @@ -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, + }, +}); diff --git a/src/scripts/js/es6/common/vuecomponents/shotstable/cells/renderer/Picture.js b/src/scripts/js/es6/common/vuecomponents/shotstable/cells/renderer/Picture.js new file mode 100644 index 0000000..e8c7463 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/shotstable/cells/renderer/Picture.js @@ -0,0 +1,63 @@ +let CellDefault = pillar.vuecomponents.table.cells.renderer.CellDefault; + +const TEMPLATE =` +
+ + +
+`; + +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 } diff --git a/src/scripts/js/es6/common/vuecomponents/shotstable/columns/Picture.js b/src/scripts/js/es6/common/vuecomponents/shotstable/columns/Picture.js new file mode 100644 index 0000000..8a473fe --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/shotstable/columns/Picture.js @@ -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; + } +} diff --git a/src/scripts/js/es6/common/vuecomponents/shotstable/columns/ShotsColumnFactory.js b/src/scripts/js/es6/common/vuecomponents/shotstable/columns/ShotsColumnFactory.js new file mode 100644 index 0000000..7098523 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/shotstable/columns/ShotsColumnFactory.js @@ -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 } diff --git a/src/scripts/js/es6/common/vuecomponents/shotstable/rows/ShotRow.js b/src/scripts/js/es6/common/vuecomponents/shotstable/rows/ShotRow.js new file mode 100644 index 0000000..0aba72f --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/shotstable/rows/ShotRow.js @@ -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 } diff --git a/src/scripts/js/es6/common/vuecomponents/shotstable/rows/ShotRowsSource.js b/src/scripts/js/es6/common/vuecomponents/shotstable/rows/ShotRowsSource.js new file mode 100644 index 0000000..ff94094 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/shotstable/rows/ShotRowsSource.js @@ -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 } diff --git a/src/scripts/js/es6/common/vuecomponents/taskstable/Table.js b/src/scripts/js/es6/common/vuecomponents/taskstable/Table.js new file mode 100644 index 0000000..4e3efd0 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/taskstable/Table.js @@ -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 =` +
+ +
+`; + +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, + } +}); diff --git a/src/scripts/js/es6/common/vuecomponents/taskstable/cells/ParentName.js b/src/scripts/js/es6/common/vuecomponents/taskstable/cells/ParentName.js new file mode 100644 index 0000000..3885fe2 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/taskstable/cells/ParentName.js @@ -0,0 +1,42 @@ +let CellDefault = pillar.vuecomponents.table.cells.renderer.CellDefault; + +const TEMPLATE =` +
+`; + +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 } diff --git a/src/scripts/js/es6/common/vuecomponents/taskstable/columns/ParentName.js b/src/scripts/js/es6/common/vuecomponents/taskstable/columns/ParentName.js new file mode 100644 index 0000000..1e378d0 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/taskstable/columns/ParentName.js @@ -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 || ''; + } + + 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 } diff --git a/src/scripts/js/es6/common/vuecomponents/taskstable/columns/ShortCode.js b/src/scripts/js/es6/common/vuecomponents/taskstable/columns/ShortCode.js new file mode 100644 index 0000000..2146cde --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/taskstable/columns/ShortCode.js @@ -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 } diff --git a/src/scripts/js/es6/common/vuecomponents/taskstable/columns/TaskType.js b/src/scripts/js/es6/common/vuecomponents/taskstable/columns/TaskType.js new file mode 100644 index 0000000..2818627 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/taskstable/columns/TaskType.js @@ -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 } diff --git a/src/scripts/js/es6/common/vuecomponents/taskstable/columns/TasksColumnFactory.js b/src/scripts/js/es6/common/vuecomponents/taskstable/columns/TasksColumnFactory.js new file mode 100644 index 0000000..a9717a0 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/taskstable/columns/TasksColumnFactory.js @@ -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 } diff --git a/src/scripts/js/es6/common/vuecomponents/taskstable/rows/TaskRow.js b/src/scripts/js/es6/common/vuecomponents/taskstable/rows/TaskRow.js new file mode 100644 index 0000000..6bc02e8 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/taskstable/rows/TaskRow.js @@ -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 } diff --git a/src/scripts/js/es6/common/vuecomponents/taskstable/rows/TaskRowsSource.js b/src/scripts/js/es6/common/vuecomponents/taskstable/rows/TaskRowsSource.js new file mode 100644 index 0000000..06699b5 --- /dev/null +++ b/src/scripts/js/es6/common/vuecomponents/taskstable/rows/TaskRowsSource.js @@ -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 } diff --git a/src/scripts/tutti/10_tasks.js b/src/scripts/tutti/10_tasks.js index 16a41d0..6afea34 100644 --- a/src/scripts/tutti/10_tasks.js +++ b/src/scripts/tutti/10_tasks.js @@ -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('\ - \ - \ - -save your task first-\ - -\ - \ - '); - } 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('\ - \ - \ - '); - - $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())) { diff --git a/src/styles/components/_attract_table.sass b/src/styles/components/_attract_table.sass new file mode 100644 index 0000000..0a0fe42 --- /dev/null +++ b/src/styles/components/_attract_table.sass @@ -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 diff --git a/src/styles/main.sass b/src/styles/main.sass index 0ec49bd..18e1bed 100644 --- a/src/styles/main.sass +++ b/src/styles/main.sass @@ -55,6 +55,9 @@ @import _app_utils @import _app_base +// Attract components +@import "components/attract_table" + @import _tasks @import _shots @import _dashboard diff --git a/src/templates/attract/assets/for_project.pug b/src/templates/attract/assets/for_project.pug index e7f8c10..1a5fa8e 100644 --- a/src/templates/attract/assets/for_project.pug +++ b/src/templates/attract/assets/for_project.pug @@ -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 */ - $('
').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 %} diff --git a/src/templates/attract/assets/view_asset_embed.pug b/src/templates/attract/assets/view_asset_embed.pug index 649ad46..fa61428 100644 --- a/src/templates/attract/assets/view_asset_embed.pug +++ b/src/templates/attract/assets/view_asset_embed.pug @@ -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']) }}"; diff --git a/src/templates/attract/shots/for_project.pug b/src/templates/attract/shots/for_project.pug index 6ee5dc1..d7bfecf 100644 --- a/src/templates/attract/shots/for_project.pug +++ b/src/templates/attract/shots/for_project.pug @@ -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 */ - $('
').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 %} diff --git a/src/templates/attract/shots/view_shot_embed.pug b/src/templates/attract/shots/view_shot_embed.pug index ce59bc6..e8ca28f 100644 --- a/src/templates/attract/shots/view_shot_embed.pug +++ b/src/templates/attract/shots/view_shot_embed.pug @@ -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']) }}"; diff --git a/src/templates/attract/tasks/for_project.pug b/src/templates/attract/tasks/for_project.pug index eb64b7f..424f606 100644 --- a/src/templates/attract/tasks/for_project.pug +++ b/src/templates/attract/tasks/for_project.pug @@ -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 %} diff --git a/src/templates/attract/tasks/view_task_embed.pug b/src/templates/attract/tasks/view_task_embed.pug index 32adcec..da16915 100644 --- a/src/templates/attract/tasks/view_task_embed.pug +++ b/src/templates/attract/tasks/view_task_embed.pug @@ -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