Skip to content
This repository has been archived by the owner on Nov 28, 2022. It is now read-only.

Tags screen refresh #1283

Merged
merged 25 commits into from
Aug 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
40e94e0
Added new gh-tags-list component
rishabhgrg Aug 21, 2019
9101f74
Updated parent tags route/controller/template
rishabhgrg Aug 21, 2019
d5d8a8c
Updated tag detail and new tag screens
rishabhgrg Aug 21, 2019
b30cccd
Added slugify method for tag slug generation on admin before save
rishabhgrg Aug 21, 2019
1774373
Tags screen refinements and illustration for empty screen
peterzimon Aug 21, 2019
3936367
Refined empty state for tags
peterzimon Aug 21, 2019
eade52e
Removed old gh-tag component
rishabhgrg Aug 22, 2019
4d4c315
Removed unused css styles
rishabhgrg Aug 23, 2019
3610e0d
Tags css cleanup
rishabhgrg Aug 23, 2019
c8f17fc
Updated class names for tags
rishabhgrg Aug 26, 2019
3d73d45
Skip old tags test
rishabhgrg Aug 26, 2019
cfc399c
Removed tag deletion logic for tag list
rishabhgrg Aug 26, 2019
3911110
Removed `scrollIntoView` for tag list
rishabhgrg Aug 27, 2019
d1b337e
Added vertical collection for tags list
rishabhgrg Aug 27, 2019
a1c9343
Removed unused init override
rishabhgrg Aug 27, 2019
815c12c
Removed default route file for tags.index
rishabhgrg Aug 27, 2019
5fbf67a
Removed unused jquery import
rishabhgrg Aug 27, 2019
5bc856d
Removed unused closure methods
rishabhgrg Aug 27, 2019
4ddc63f
Updated slug max-width in tags list
peterzimon Aug 27, 2019
432d22e
Removed unused keyboard navigation handling
kevinansfield Aug 27, 2019
d544ec9
tweak vertical-collection settings
kevinansfield Aug 27, 2019
001baa4
fixed vertical-collection rendering
kevinansfield Aug 27, 2019
770feaa
use templateName instead of renderTemplate
kevinansfield Aug 27, 2019
3fabd61
Updated new empty tag unsaved modal behavior
rishabhgrg Aug 27, 2019
d3be2b8
Fixed lint
rishabhgrg Aug 27, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 0 additions & 5 deletions app/components/gh-tag.js

This file was deleted.

38 changes: 38 additions & 0 deletions app/components/gh-tags-list-item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Component from '@ember/component';
import {alias} from '@ember/object/computed';
import {computed} from '@ember/object';
import {inject as service} from '@ember/service';

export default Component.extend({
ghostPaths: service(),
notifications: service(),
router: service(),

tagName: 'li',
classNames: ['gh-list-row', 'gh-tags-list-item'],

active: false,

id: alias('tag.id'),
slug: alias('tag.slug'),
name: alias('tag.name'),
isInternal: alias('tag.isInternal'),
description: alias('tag.description'),
postsCount: alias('tag.count.posts'),
postsLabel: computed('tag.count.posts', function () {
let noOfPosts = this.postsCount || 0;
return (noOfPosts === 1) ? `${noOfPosts} post` : `${noOfPosts} posts`;
}),

_deleteTag() {
let tag = this.tag;

return tag.destroyRecord().then(() => {}, (error) => {
this._deleteTagFailure(error);
});
},

_deleteTagFailure(error) {
this.notifications.showAPIError(error, {key: 'tag.delete'});
}
});
47 changes: 9 additions & 38 deletions app/controllers/tags.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import Controller, {inject as controller} from '@ember/controller';
import {alias, equal, sort} from '@ember/object/computed';
import {alias, sort} from '@ember/object/computed';
import {computed} from '@ember/object';
import {run} from '@ember/runloop';

export default Controller.extend({

tagController: controller('tags.tag'),

queryParams: ['type'],
type: 'public',
tags: alias('model'),
selectedTag: alias('tagController.tag'),

tagListFocused: equal('keyboardFocus', 'tagList'),
tagContentFocused: equal('keyboardFocus', 'tagContent'),

filteredTags: computed('tags.@each.isNew', function () {
return this.tags.filterBy('isNew', false);
filteredTags: computed('tags.@each.isNew', 'type', function () {
return this.tags.filter((tag) => {
return (!tag.isNew && (!this.type || tag.visibility === this.type));
});
}),

// tags are sorted by name
Expand All @@ -24,37 +24,8 @@ export default Controller.extend({
}),

actions: {
leftMobile() {
let firstTag = this.get('tags.firstObject');
// redirect to first tag if possible so that you're not left with
// tag settings blank slate when switching from portrait to landscape
if (firstTag && !this.get('tagController.tag')) {
this.transitionToRoute('tags.tag', firstTag);
}
changeType(type) {
this.set('type', type);
}
},

scrollTagIntoView(tag) {
run.scheduleOnce('afterRender', this, function () {
let id = `#gh-tag-${tag.get('id')}`;
let element = document.querySelector(id);

if (element) {
let scroll = document.querySelector('.tag-list');
let {scrollTop} = scroll;
let scrollHeight = scroll.offsetHeight;
let element = document.querySelector(id);
let elementTop = element.offsetTop;
let elementHeight = element.offsetHeight;

if (elementTop < scrollTop) {
element.scrollIntoView(true);
}

if (elementTop + elementHeight > scrollTop + scrollHeight) {
element.scrollIntoView(false);
}
}
});
}
});
73 changes: 65 additions & 8 deletions app/controllers/tags/tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import Controller, {inject as controller} from '@ember/controller';
import windowProxy from 'ghost-admin/utils/window-proxy';
import {alias} from '@ember/object/computed';
import {inject as service} from '@ember/service';
import {slugify} from '@tryghost/string';
import {task} from 'ember-concurrency';

export default Controller.extend({
tagsController: controller('tags'),
Expand All @@ -24,6 +26,47 @@ export default Controller.extend({

deleteTag() {
return this._deleteTag();
},
save() {
return this.save.perform();
},

toggleUnsavedChangesModal(transition) {
let leaveTransition = this.leaveScreenTransition;

if (!transition && this.showUnsavedChangesModal) {
this.set('leaveScreenTransition', null);
this.set('showUnsavedChangesModal', false);
return;
}

if (!leaveTransition || transition.targetName === leaveTransition.targetName) {
this.set('leaveScreenTransition', transition);

// if a save is running, wait for it to finish then transition
if (this.save.isRunning) {
return this.save.last.then(() => {
transition.retry();
});
}

// we genuinely have unsaved data, show the modal
this.set('showUnsavedChangesModal', true);
}
},

leaveScreen() {
let transition = this.leaveScreenTransition;

if (!transition) {
this.notifications.showAlert('Sorry, there was an error in the application. Please let the Ghost team know what happened.', {type: 'error'});
return;
}

// roll back changes on model props
this.tag.rollbackAttributes();

return transition.retry();
}
},

Expand All @@ -42,29 +85,43 @@ export default Controller.extend({
}

tag.set(propKey, newValue);

// Generate slug based on name for new tag when empty
if (propKey === 'name' && !tag.get('slug') && isNewTag) {
let slugValue = slugify(newValue);
tag.set('slug', slugValue);
}
// TODO: This is required until .validate/.save mark fields as validated
tag.get('hasValidated').addObject(propKey);
},

tag.save().then((savedTag) => {
save: task(function* () {
let tag = this.tag;
let isNewTag = tag.get('isNew');
try {
let savedTag = yield tag.save();
// replace 'new' route with 'tag' route
this.replaceRoute('tags.tag', savedTag);

// update the URL if the slug changed
if (propKey === 'slug' && !isNewTag) {
if (!isNewTag) {
let currentPath = window.location.hash;

let newPath = currentPath.split('/');
newPath[newPath.length - 1] = savedTag.get('slug');
newPath = newPath.join('/');
if (newPath[newPath.length - 1] !== savedTag.get('slug')) {
newPath[newPath.length - 1] = savedTag.get('slug');
newPath = newPath.join('/');

windowProxy.replaceState({path: newPath}, '', newPath);
windowProxy.replaceState({path: newPath}, '', newPath);
}
}
}).catch((error) => {
return savedTag;
} catch (error) {
if (error) {
this.notifications.showAPIError(error, {key: 'tag.save'});
}
});
},
}
}),

_deleteTag() {
let tag = this.tag;
Expand Down
73 changes: 6 additions & 67 deletions app/routes/tags.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
/* global key */
import $ from 'jquery';
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
import CurrentUserSettings from 'ghost-admin/mixins/current-user-settings';
import ShortcutsRoute from 'ghost-admin/mixins/shortcuts-route';

export default AuthenticatedRoute.extend(CurrentUserSettings, ShortcutsRoute, {
queryParams: {
type: {
refreshModel: true,
replace: true
}
},

shortcuts: null,

init() {
this._super(...arguments);
this.shortcuts = {
'up, k': 'moveUp',
'down, j': 'moveDown',
left: 'focusList',
right: 'focusContent',
c: 'newTag'
};
},
Expand All @@ -33,83 +33,22 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, ShortcutsRoute, {
model() {
let promise = this.store.query('tag', {limit: 'all', include: 'count.posts'});
let tags = this.store.peekAll('tag');

if (this.store.peekAll('tag').get('length') === 0) {
return promise.then(() => tags);
} else {
return tags;
}
},

deactivate() {
this._super(...arguments);
if (!this.isDestroyed && !this.isDestroying) {
this.send('resetShortcutsScope');
}
},

actions: {
moveUp() {
if (this.controller.get('tagContentFocused')) {
this.scrollContent(-1);
} else {
this.stepThroughTags(-1);
}
},

moveDown() {
if (this.controller.get('tagContentFocused')) {
this.scrollContent(1);
} else {
this.stepThroughTags(1);
}
},

focusList() {
this.set('controller.keyboardFocus', 'tagList');
},

focusContent() {
this.set('controller.keyboardFocus', 'tagContent');
},

newTag() {
this.transitionTo('tags.new');
},

resetShortcutsScope() {
key.setScope('default');
}
},

buildRouteInfoMetadata() {
return {
titleToken: 'Tags'
};
},

stepThroughTags(step) {
let currentTag = this.modelFor('tags.tag');
let tags = this.get('controller.sortedTags');
let length = tags.get('length');

if (currentTag && length) {
let newPosition = tags.indexOf(currentTag) + step;

if (newPosition >= length) {
return;
} else if (newPosition < 0) {
return;
}

this.transitionTo('tags.tag', tags.objectAt(newPosition));
}
},

scrollContent(amount) {
let content = $('.tag-settings-pane');
let scrolled = content.scrollTop();

content.scrollTop(scrolled + 50 * amount);
}
});
16 changes: 0 additions & 16 deletions app/routes/tags/index.js

This file was deleted.

28 changes: 24 additions & 4 deletions app/routes/tags/new.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
import {isEmpty} from '@ember/utils';
import {inject as service} from '@ember/service';

export default AuthenticatedRoute.extend({

router: service(),

controllerName: 'tags.tag',
templateName: 'tags/tag',

model() {
return this.store.createRecord('tag');
init() {
this._super(...arguments);
this.router.on('routeWillChange', (transition) => {
this.showUnsavedChangesModal(transition);
});
},

renderTemplate() {
this.render('tags.tag');
model() {
return this.store.createRecord('tag');
},

// reset the model so that mobile screens react to an empty selectedTag
Expand All @@ -19,6 +27,18 @@ export default AuthenticatedRoute.extend({
let {controller} = this;
controller.model.rollbackAttributes();
controller.set('model', null);
},

showUnsavedChangesModal(transition) {
if (transition.from && transition.from.name.match(/^tags\.new/) && transition.targetName) {
let {controller} = this;
let isUnchanged = isEmpty(Object.keys(controller.tag.changedAttributes()));
if (!controller.tag.isDeleted && !isUnchanged) {
transition.abort();
controller.send('toggleUnsavedChangesModal', transition);
return;
}
}
}

});