0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Removed Collections code from Admin

ref https://linear.app/ghost/issue/ENG-1805/remove-collections-code

- we're removing this feature as it's not finished to the degree we
  would like, so this commit removes all references to it from Admin in
  order to keep things clean
This commit is contained in:
Daniel Lockyer 2024-12-04 17:05:15 +01:00 committed by Daniel Lockyer
parent ed6c57e2a0
commit 4c13f188ce
22 changed files with 1 additions and 613 deletions

View file

@ -19,10 +19,6 @@ const features = [{
title: 'Email customization',
description: 'Adding more control over the newsletter template',
flag: 'emailCustomization'
},{
title: 'Collections',
description: 'Enables Collections 2.0',
flag: 'collections'
},{
title: 'Collections Card',
description: 'Enables the Collections Card for pages - requires Collections and the beta Editor to be enabled',

View file

@ -1,90 +0,0 @@
<div class="gh-main-section">
<div class="flex justify-between items-center">
<h4 class="gh-main-section-header small bn">Basic settings</h4>
</div>
<section class="gh-main-section-block">
<div class="gh-main-section-content grey columns-1">
<GhFormGroup @errors={{@collection.errors}} @hasValidated={{@collection.hasValidated}} @property="title" class="mr2 flex-auto">
<label for="collection-title">Title</label>
<input type="text" class="gh-input" id="collection-title" name="title" value={{@collection.title}} {{on "input" (pick "target.value"
(fn this.setCollectionProperty "title" ))}} {{on "blur" (fn this.validateCollectionProperty "title" )}}
data-test-input="collection-title" />
<span class="error">
<GhErrorMessage @errors={{@collection.errors}} @property="title" />
</span>
</GhFormGroup>
<GhFormGroup @errors={{@collection.errors}} @hasValidated={{@collection.hasValidated}} @property="slug" class="mr2 flex-auto">
<label for="collection-slug">Slug</label>
<input type="text" class="gh-input" id="collection-slug" name="slug" value={{@collection.slug}} {{on "input" (pick "target.value"
(fn this.setCollectionProperty "slug" ))}} {{on "blur" (fn this.validateCollectionProperty "slug" )}}
data-test-input="collection-slug" />
<span class="error">
<GhErrorMessage @errors={{@collection.errors}} @property="slug" />
</span>
</GhFormGroup>
<GhFormGroup class="no-margin" @errors={{@collection.errors}} @hasValidated={{@collection.hasValidated}}
@property="description">
<label for="collection-description">Description</label>
<textarea id="collection-description" name="description" class="gh-input gh-collection-details-textarea"
{{on "input" (pick "target.value" (fn this.setCollectionProperty "description" ))}} {{on "blur" (fn
this.validateCollectionProperty "description" )}}
data-test-input="collection-description">{{@collection.description}}</textarea>
<GhErrorMessage @errors={{@collection.errors}} @property="description" />
<p>Maximum: <b>500</b> characters. Youve used {{gh-count-down-characters @collection.description 500}}</p>
</GhFormGroup>
<GhFormGroup class="gh-collection-image-uploader no-margin" @errors={{@collection.errors}} @hasValidated={{@collection.hasValidated}} @property="featureImage">
<label for="collection-image">Collection image</label>
<GhImageUploaderWithPreview
@image={{@collection.featureImage}}
@text="Upload collection image"
@allowUnsplash={{true}}
@update={{fn this.setCollectionProperty "featureImage"}}
@remove={{fn this.setCollectionProperty "featureImage" ""}}
/>
</GhFormGroup>
</div>
</section>
<section class="gh-main-section-block">
<div class="gh-main-section-content grey columns-1">
<GhFormGroup class="gh-collection-image-uploader no-margin" @errors={{@collection.errors}} @hasValidated={{@collection.hasValidated}} @property="type">
<label for="collection-image">Collection type</label>
<div class="gh-contentfilter-menu gh-contentfilter-visibility {{if @selectedVisibility.value "gh-contentfilter-selected"}}" data-test-visibility-select="true">
<PowerSelect
@selected={{this.selectedType}}
@options={{this.availableTypes}}
@searchEnabled={{false}}
@onChange={{this.changeType}}
@triggerComponent={{component "gh-power-select/trigger"}}
@triggerClass="gh-contentfilter-menu-trigger"
@dropdownClass="gh-contentfilter-menu-dropdown"
@matchTriggerWidth={{false}}
as |type|
>
{{#if type.name}}{{type.name}}{{else}}<span class="red">Unknown type</span>{{/if}}
</PowerSelect>
{{#if (eq this.selectedType.value 'manual')}}
<p>Add posts to this collection one by one through post settings menu.</p>
{{/if}}
{{#if (eq this.selectedType.value 'automatic')}}
<input type="text"
class="gh-input"
id="collection-filter"
name="filter"
value={{@collection.filter}}
{{on "input" (pick "target.value" (fn this.setCollectionProperty "filter" ))}}
{{on "blur" (fn this.validateCollectionProperty "filter" )}}
data-test-input="collection-filter" />
{{/if}}
</div>
</GhFormGroup>
</div>
</section>
</div>

View file

@ -1,65 +0,0 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {slugify} from '@tryghost/string';
const TYPES = [{
name: 'Manual',
value: 'manual'
}, {
name: 'Automatic',
value: 'automatic'
}];
export default class CollectionForm extends Component {
@service feature;
@service settings;
@inject config;
availableTypes = TYPES;
get selectedType() {
const {collection} = this.args;
return this.availableTypes.findBy('value', collection.type) || {value: '!unknown'};
}
@action
setCollectionProperty(property, newValue) {
const {collection} = this.args;
if (newValue) {
newValue = newValue.trim();
}
// Generate slug based on name for new collection when empty
if (property === 'title' && collection.isNew && !this.hasChangedSlug) {
let slugValue = slugify(newValue);
if (/^#/.test(newValue)) {
slugValue = 'hash-' + slugValue;
}
collection.slug = slugValue;
}
// ensure manual changes of slug don't get reset when changing name
if (property === 'slug') {
this.hasChangedSlug = !!newValue;
}
collection[property] = newValue;
// clear validation message when typing
collection.hasValidated.addObject(property);
}
@action
changeType(type) {
this.setCollectionProperty('type', type.value);
}
@action
validateCollectionProperty(property) {
return this.args.collection.validate({property});
}
}

View file

@ -1,19 +0,0 @@
<div class="modal-content" {{on-key "Enter" (perform this.deleteCollectionTask)}} data-test-modal="confirm-delete-collection">
<header class="modal-header">
<h1>Are you sure you want to delete this collection?</h1>
</header>
<button type="button" class="close"title="Close" {{on "click" @close}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
<div class="modal-body">
{{#if @data.collection.count.posts}}
<span class="red">This collection is attached to <span data-test-text="posts-count">{{gh-pluralize @data.collection.count.posts "post"}}</span>.</span>
{{/if}}
Youre about to delete "<strong>{{@data.collection.title}}</strong>". This is permanent! We warned you, k?
</div>
<div class="modal-footer">
<button class="gh-btn" type="button" {{on "click" @close}} data-test-button="cancel"><span>Cancel</span></button>
<GhTaskButton @buttonText="Delete" @successText="Deleted" @task={{this.deleteCollectionTask}}
@class="gh-btn gh-btn-red gh-btn-icon" data-test-button="confirm" />
</div>
</div>

View file

@ -1,29 +0,0 @@
import Component from '@glimmer/component';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
export default class DeleteCollectionModal extends Component {
@service notifications;
@service router;
@task({drop: true})
*deleteCollectionTask() {
try {
const {collection} = this.args.data;
if (collection.isDeleted) {
return true;
}
yield collection.destroyRecord();
this.notifications.closeAlerts('collection.delete');
this.router.transitionTo('collections');
return true;
} catch (error) {
this.notifications.showAPIError(error, {key: 'collection.delete.failed'});
} finally {
this.args.close();
}
}
}

View file

@ -1,32 +0,0 @@
<li class="gh-list-row gh-collections-list-item" ...attributes>
<LinkTo @route="collection" @model={{@collection}} class="gh-list-data gh-collection-list-title gh-list-cellwidth-70" title="Edit collection">
<h3 class="gh-collection-list-name" data-test-collection-name>
{{@collection.title}}
</h3>
{{#if @collection.description}}
<p class="ma0 pa0 f8 midgrey gh-collection-list-description" data-test-collection-description>
{{@collection.description}}
</p>
{{/if}}
</LinkTo>
<LinkTo @route="collection" @model={{@collection}} class="gh-list-data middarkgrey f8 gh-collection-list-slug gh-list-cellwidth-10" title="Edit collection" data-test-collection-slug>
<span title="{{@collection.slug}}">{{@collection.slug}}</span>
</LinkTo>
{{#if @collection.count.posts}}
<LinkTo @route="posts" @query={{hash type=null author=null collection=@collection.slug order=null}} class="gh-list-data gh-collection-list-posts-count gh-list-cellwidth-10 f8" title={{concat "List posts collectionged with '" @collection.name "'"}}>
<span class="nowrap">{{gh-pluralize @collection.count.posts "post"}}</span>
</LinkTo>
{{else}}
<LinkTo @route="collection" @model={{@collection}} class="gh-list-data gh-collection-list-posts-count gh-list-cellwidth-10" title="Edit collection">
<span class="nowrap f8 midlightgrey">{{gh-pluralize @collection.count.posts "post"}}</span>
</LinkTo>
{{/if}}
<LinkTo @route="collection" @model={{@collection}} class="gh-list-data gh-list-cellwidth-10 gh-list-chevron" title="Edit collection">
<div class="flex items-center justify-end w-100 h-100">
<span class="nr2">{{svg-jar "arrow-right" class="w6 h6 fill-midgrey pa1"}}</span>
</div>
</LinkTo>
</li>

View file

@ -1,7 +1,6 @@
import * as Sentry from '@sentry/ember';
import Component from '@glimmer/component';
import React, {Suspense} from 'react';
import fetch from 'fetch';
import ghostPaths from 'ghost-admin/utils/ghost-paths';
import moment from 'moment-timezone';
import {action} from '@ember/object';
@ -276,24 +275,6 @@ export default class KoenigLexicalEditor extends Component {
return response;
};
const fetchCollectionPosts = async (collectionSlug) => {
if (!this.contentKey) {
const integrations = await this.store.findAll('integration');
const contentIntegration = integrations.findBy('slug', 'ghost-core-content');
this.contentKey = contentIntegration?.contentKey.secret;
}
const postsUrl = new URL(this.ghostPaths.url.admin('/api/content/posts/'), window.location.origin);
postsUrl.searchParams.append('key', this.contentKey);
postsUrl.searchParams.append('collection', collectionSlug);
postsUrl.searchParams.append('limit', 12);
const response = await fetch(postsUrl.toString());
const {posts} = await response.json();
return posts;
};
const fetchAutocompleteLinks = async () => {
const defaults = [
{label: 'Homepage', value: window.location.origin + '/'},
@ -455,13 +436,11 @@ export default class KoenigLexicalEditor extends Component {
unsplash: this.settings.unsplash ? unsplashConfig.defaultHeaders : null,
tenor: this.config.tenor?.googleApiKey ? this.config.tenor : null,
fetchAutocompleteLinks,
fetchCollectionPosts,
fetchEmbed,
fetchLabels,
renderLabels: !this.session.user.isContributor,
feature: {
collectionsCard: this.feature.collectionsCard,
collections: this.feature.collections,
contentVisibility: this.feature.contentVisibility
},
deprecated: { // todo fix typo

View file

@ -1,43 +0,0 @@
import Controller from '@ember/controller';
import DeleteCollectionModal from '../components/collections/delete-collection-modal';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
export default class CollectionController extends Controller {
@service modals;
@service notifications;
@service router;
get collection() {
return this.model;
}
@action
confirmDeleteCollection() {
return this.modals.open(DeleteCollectionModal, {
collection: this.model
});
}
@task({drop: true})
*saveTask() {
let {collection} = this;
try {
if (collection.get('errors').length !== 0) {
return;
}
yield collection.save();
// replace 'new' route with 'collection' route
this.replaceRoute('collection', collection);
return collection;
} catch (error) {
if (error) {
this.notifications.showAPIError(error, {key: 'collection.save'});
}
}
}
}

View file

@ -1,38 +0,0 @@
import Controller from '@ember/controller';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class CollectionsController extends Controller {
@service router;
queryParams = ['type'];
@tracked type = 'public';
get collections() {
return this.model;
}
get filteredCollections() {
return this.collections.filter((collection) => {
return (!collection.isNew);
});
}
get sortedCollections() {
return this.filteredCollections.sort((collectionA, collectionB) => {
// ignorePunctuation means the # in internal collection names is ignored
return collectionA.title.localeCompare(collectionB.title, undefined, {ignorePunctuation: true});
});
}
@action
changeType(type) {
this.type = type;
}
@action
newCollection() {
this.router.transitionTo('collection.new');
}
}

View file

@ -261,11 +261,6 @@ export default class LexicalEditorController extends Controller {
});
}
@computed
get collections() {
return this.store.peekAll('collection');
}
@computed('session.user.{isAdmin,isEditor}')
get canManageSnippets() {
let {user} = this.session;
@ -986,10 +981,6 @@ export default class LexicalEditorController extends Controller {
*backgroundLoaderTask() {
yield this.store.query('snippet', {limit: 'all'});
if (this.post?.displayName === 'page' && this.feature.get('collections') && this.feature.get('collectionsCard')) {
yield this.store.query('collection', {limit: 'all'});
}
this.search.refreshContentTask.perform();
this.syncMobiledocSnippets();
}
@ -1235,7 +1226,7 @@ export default class LexicalEditorController extends Controller {
const isDraft = this.post.get('status') === 'draft';
const slugContainsUntitled = slug.includes('untitled');
const isTitleSet = title && title.trim() !== '' && title !== DEFAULT_TITLE;
if (isDraft && slugContainsUntitled && isTitleSet) {
Sentry.captureException(new Error('Draft post has title set with untitled slug'), {
extra: {

View file

@ -1,6 +1,5 @@
// TODO: remove usage of Ember Data's private `Errors` class when refactoring validations
// eslint-disable-next-line
import CollectionValidator from 'ghost-admin/validators/collection';
import CustomViewValidator from 'ghost-admin/validators/custom-view';
import DS from 'ember-data'; // eslint-disable-line
import IntegrationValidator from 'ghost-admin/validators/integration';
@ -68,7 +67,6 @@ export default Mixin.create({
signin: SigninValidator,
signup: SignupValidator,
tag: TagSettingsValidator,
collection: CollectionValidator,
user: UserValidator,
member: MemberValidator,
integration: IntegrationValidator,

View file

@ -48,10 +48,6 @@ Router.map(function () {
this.route('tag.new', {path: '/tags/new'});
this.route('tag', {path: '/tags/:tag_slug'});
this.route('collections');
this.route('collection.new', {path: '/collections/new'});
this.route('collection', {path: '/collections/:collection_slug'});
this.route('demo-x', function () {
this.route('demo-x', {path: '/*sub'});
});

View file

@ -1,93 +0,0 @@
import * as Sentry from '@sentry/ember';
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
import ConfirmUnsavedChangesModal from '../components/modals/confirm-unsaved-changes';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
export default class CollectionRoute extends AuthenticatedRoute {
@service modals;
@service router;
@service session;
// ensures if a tag model is passed in directly we show it immediately
// and refresh in the background
_requiresBackgroundRefresh = true;
beforeModel() {
super.beforeModel(...arguments);
if (this.session.user.isAuthorOrContributor) {
return this.transitionTo('home');
}
}
model(params) {
this._requiresBackgroundRefresh = false;
if (params.collection_slug) {
return this.store.queryRecord('collection', {slug: params.collection_slug});
} else {
return this.store.createRecord('collection');
}
}
serialize(collection) {
return {collection_slug: collection.get('slug')};
}
setupController(controller, tag) {
super.setupController(...arguments);
if (this._requiresBackgroundRefresh) {
tag.reload();
}
}
deactivate() {
this._requiresBackgroundRefresh = true;
this.confirmModal = null;
this.hasConfirmed = false;
}
@action
async willTransition(transition) {
if (this.hasConfirmed) {
return true;
}
transition.abort();
// wait for any existing confirm modal to be closed before allowing transition
if (this.confirmModal) {
return;
}
if (this.controller.saveTask?.isRunning) {
await this.controller.saveTask.last;
}
const shouldLeave = await this.confirmUnsavedChanges();
if (shouldLeave) {
this.controller.model.rollbackAttributes();
this.hasConfirmed = true;
return transition.retry();
}
}
async confirmUnsavedChanges() {
if (this.controller.model?.hasDirtyAttributes) {
Sentry.captureMessage('showing unsaved changes modal for collections route');
this.confirmModal = this.modals
.open(ConfirmUnsavedChangesModal)
.finally(() => {
this.confirmModal = null;
});
return this.confirmModal;
}
return true;
}
}

View file

@ -1,6 +0,0 @@
import CollectionRoute from '../collection';
export default class NewRoute extends CollectionRoute {
controllerName = 'collection';
templateName = 'collection';
}

View file

@ -1,31 +0,0 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
export default class CollectionsRoute extends AuthenticatedRoute {
// authors aren't allowed to manage tags
beforeModel() {
super.beforeModel(...arguments);
if (this.session.user.isAuthorOrContributor) {
return this.transitionTo('home');
}
}
// set model to a live array so all collections are shown and created/deleted collections
// are automatically added/removed. Also load all collections in the background,
// pausing to show the loading spinner if no collections have been loaded yet
model() {
let promise = this.store.query('collection', {limit: 'all', include: 'count.posts'});
let collections = this.store.peekAll('collection');
if (this.store.peekAll('collection').get('length') === 0) {
return promise.then(() => collections);
} else {
return collections;
}
}
buildRouteInfoMetadata() {
return {
titleToken: 'Collections'
};
}
}

View file

@ -67,7 +67,6 @@ export default class FeatureService extends Service {
@feature('i18n') i18n;
@feature('announcementBar') announcementBar;
@feature('signupCard') signupCard;
@feature('collections') collections;
@feature('mailEvents') mailEvents;
@feature('collectionsCard') collectionsCard;
@feature('importMemberTier') importMemberTier;

View file

@ -1,48 +0,0 @@
<section class="gh-canvas">
<form class="mb15">
<GhCanvasHeader class="gh-canvas-header">
<div class="flex flex-column">
<div class="gh-canvas-breadcrumb">
<LinkTo @route="collections" data-test-link="collections-back">
Collections
</LinkTo>
{{svg-jar "arrow-right-small"}} {{if this.collection.isNew "New collection" "Edit collection"}}
</div>
<h2 class="gh-canvas-title" data-test-screen-title>
{{if this.collection.isNew "New collection" this.collection.title}}
</h2>
</div>
<section class="view-actions">
<GhTaskButton
@task={{this.saveTask}}
@type="button"
@class="gh-btn gh-btn-primary gh-btn-icon"
@data-test-button="save"
{{on-key "cmd+s"}}
/>
</section>
</GhCanvasHeader>
<Collections::CollectionForm @collection={{this.model}} />
</form>
{{#unless this.collection.isNew}}
<div>
<button type="button" class="gh-btn gh-btn-red gh-btn-icon" {{on "click" this.confirmDeleteCollection}} data-test-button="delete-collection">
<span>Delete collection</span>
</button>
</div>
{{/unless}}
{{#if this.collection.postIds}}
<div class="gh-main-section">
<h3>Collection has {{this.collection.postIds.length}} posts</h3>
<ol class="gh-list">
{{#each this.collection.postIds as |post|}}
<li class="gh-list-row"><a href="#/editor/post/{{post}}">{{post}}</a></li>
{{/each}}
</ol>
</div>
{{/if}}
</section>

View file

@ -1,34 +0,0 @@
<section class="gh-canvas" {{on-key "c" this.newCollection}}>
<GhCanvasHeader class="gh-canvas-header sticky">
<h2 class="gh-canvas-title" data-test-screen-title>Collections</h2>
<section class="view-actions">
<LinkTo @route="collection.new" class="gh-btn gh-btn-primary"><span>New collection</span></LinkTo>
</section>
</GhCanvasHeader>
<section class="view-container content-list">
<ol class="collections-list gh-list {{unless this.sortedCollections "no-posts"}}">
{{#if this.sortedCollections}}
<li class="gh-list-row header">
<div class="gh-list-header gh-list-cellwidth-70">Collection</div>
<div class="gh-list-header gh-list-cellwidth-10"></div>
</li>
<VerticalCollection @items={{this.sortedCollections}} @key="id" @containerSelector=".gh-main" @estimateHeight={{60}} @bufferSize={{20}} as |collection|>
<Collections::ListItem @collection={{collection}} data-test-collection={{collection.id}} />
</VerticalCollection>
{{else}}
<li class="no-posts-box">
<div class="no-posts">
{{svg-jar "collections-placeholder" class="gh-collections-placeholder"}}
<h4>Start organizing your content.</h4>
<LinkTo @route="collection.new" class="gh-btn gh-btn-green">
<span>Create a new collection</span>
</LinkTo>
</div>
</li>
{{/if}}
</ol>
</section>
</section>
{{outlet}}

View file

@ -93,7 +93,6 @@
@cardOptions={{hash
post=this.post
snippets=this.snippets
collections=this.collections
deleteSnippet=(if this.canManageSnippets this.confirmDeleteSnippet)
createSnippet=(if this.canManageSnippets this.createSnippet)
}}

View file

@ -1,18 +0,0 @@
import BaseValidator from './base';
import {isBlank} from '@ember/utils';
export default BaseValidator.create({
properties: ['title'],
name(model) {
let title = model.title;
let hasValidated = model.hasValidated;
if (isBlank(title)) {
model.errors.add('title', 'Please enter a title.');
this.invalidate();
}
hasValidated.addObject('title');
}
});

View file

@ -1,12 +0,0 @@
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupTest} from 'ember-mocha';
describe('Unit | Route | collection', function () {
setupTest();
it('exists', function () {
let route = this.owner.lookup('route:collection');
expect(route).to.be.ok;
});
});

View file

@ -1,12 +0,0 @@
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupTest} from 'ember-mocha';
describe('Unit | Route | collections', function () {
setupTest();
it('exists', function () {
let route = this.owner.lookup('route:collections');
expect(route).to.be.ok;
});
});