Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Content profile icons #4594

Merged
merged 12 commits into from
Aug 15, 2024
49 changes: 49 additions & 0 deletions e2e/client/playwright/content-profile.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {test, expect} from '@playwright/test';
import {Monitoring} from './page-object-models/monitoring';
import {restoreDatabaseSnapshot, s} from './utils';

test('content profile icon', async ({page}) => {
const monitoring = new Monitoring(page);

await restoreDatabaseSnapshot();

// expect an article to have a regular text icon

await page.goto('/#/workspace/monitoring');
await monitoring.selectDeskOrWorkspace('Sports');

await expect(
page.locator(s(
'monitoring-group=Sports / Working Stage',
'article-item=test sports story',
'type-icon',
)),
).toHaveAttribute('data-test-value', 'text');


// go to content profile and set icon to "map-marker"

await page.goto('/#/settings/content-profiles');
await page.locator(s('content-profile=Story', 'content-profile-actions')).click();
await page.locator(s('content-profile-actions--options')).getByRole('button', {name: 'Edit'}).click();

await page.locator(s('content-profile-edit-view')).getByLabel('Icon').getByRole('button').click();
await page.getByRole('button', {name: 'map-marker'}).click();
await page.locator(s('content-profile-edit-view--footer')).getByRole('button', {name: 'Save'}).click();

await expect(page.locator(s('content-profile=Story', 'icon'))).toHaveAttribute('data-test-value', 'map-marker');


// go back to monitoring and test whether the newly set icon is being used

await page.goto('/#/workspace/monitoring');
await monitoring.selectDeskOrWorkspace('Sports');

await expect(
page.locator(s(
'monitoring-group=Sports / Working Stage',
'article-item=test sports story',
'type-icon',
)),
).toHaveAttribute('data-test-value', 'map-marker', {timeout: 10000});
});
6 changes: 3 additions & 3 deletions scripts/api/content-profiles.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {IContentProfile} from 'superdesk-api';
import ng from 'core/services/ng';
import {dataStore} from 'data-store';

interface IContentProfilesApi {
get(id: IContentProfile['_id']): Promise<IContentProfile>;
get(id: IContentProfile['_id']): IContentProfile;
}

export const contentProfiles: IContentProfilesApi = {
get: (id) => ng.get('content').getType(id),
get: (id) => dataStore.contentProfiles.get(id),
};
1 change: 0 additions & 1 deletion scripts/apps/desks/views/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ <h2 class="sd-page__page-heading" translate>Desk management</h2>
<button
ng-click="openDesk('general', desk)"
title="{{:: 'Edit desk'| translate }}"
data-test-id="desk-actions--edit"
>
<i class="icon-pencil"></i>
{{:: 'Edit'| translate}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class AssignmentsComponent extends React.Component<IProps, IState> {

const deskData: IDeskData = {};

res._items.forEach((bucket) => {
res._items.forEach((bucket: any) => {
deskData[bucket.desk] = bucket.sub.map((sub) => ({
state: sub['key'],
count: sub['count'],
Expand Down
1 change: 1 addition & 0 deletions scripts/apps/master-desk/components/ListItemsComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class ListItemsComponent extends React.Component<IProps, {}> {
<TypeIcon
type={item.type}
highlight={item.highlight}
contentProfileId={item.profile}
/>
</div>
<div className="content-item__urgency-field">
Expand Down
23 changes: 22 additions & 1 deletion scripts/apps/monitoring/controllers/AggregateCtrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {each, forEach, isNil, partition, keyBy} from 'lodash';
import {gettext, getItemTypes} from 'core/utils';
import {SCHEDULED_OUTPUT, DESK_OUTPUT} from 'apps/desks/constants';
import {appConfig} from 'appConfig';
import {IMonitoringFilter, IStage, IDesk, IMonitoringGroup} from 'superdesk-api';
import {IMonitoringFilter, IStage, IDesk, IMonitoringGroup, IContentProfile} from 'superdesk-api';
import {getLabelForStage} from 'apps/workspace/content/constants';
import {getExtensionSections} from '../services/CardsService';

Expand Down Expand Up @@ -409,6 +409,27 @@ export function AggregateCtrl($scope, desks, workspaces, preferencesService, sto
$scope.$apply();
};

this.toggleContentProfileFilter = (profile: IContentProfile) => {
if (this.isContentProfileFilterActive(profile._id)) {
this.activeFilterTags[CONTENT_PROLFILE] =
this.activeFilterTags[CONTENT_PROLFILE].filter(({key}) => key !== profile._id);
} else {
this.activeFilterTags[CONTENT_PROLFILE] = (this.activeFilterTags[CONTENT_PROLFILE] ?? []).concat({
key: profile._id,
label: profile.label,
});
}

this.activeFilters.contentProfile = this.activeFilterTags[CONTENT_PROLFILE].map(({key}) => key);

updateFilterInStore();
updateFilteringCriteria();
};

this.isContentProfileFilterActive = (id: IContentProfile['_id']): boolean => {
return (this.activeFilterTags[CONTENT_PROLFILE] ?? []).find(({key}) => key === id) != null;
};

this.setCustomFilter = (filter: IMonitoringFilter) => {
if (typeof this.activeFilters.customFilters === 'undefined') {
this.activeFilters.customFilters = {};
Expand Down
15 changes: 14 additions & 1 deletion scripts/apps/monitoring/views/monitoring-view.html
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,20 @@ <h2 class="subnav__page-title sd-flex-no-grow sd-empty" ng-hide="monitoring.sing
<div ng-if="(!elementState || elementState === 'comfort') && customDataSource == null" class="button-list button-list--padded">
<a href="" ng-repeat="fileType in aggregate.fileTypes" ng-click="aggregate.setFilterType('file', fileType.type)" class="toggle-button" ng-class="{'toggle-button--active': aggregate.hasFileType(fileType.type)}" title="{{ fileType.label }}" aria-label="{{ fileType.label }}">
<span ng-if="fileType.type === 'all'" class="toggle-button__text toggle-button__text--all">{{ fileType.label }}</span>
<i ng-if="fileType.type !== 'all'" class="toggle-button__icon filetype-icon-{{fileType.type}}" title="{{ fileType.label }}"></i>
<i ng-if="fileType.type !== 'all'" class="toggle-button__icon filetype-icon-{{fileType.type}}" aria-hidden="true"></i>
</a>

<a
href=""
ng-repeat="profile in aggregate.activeProfiles"
ng-if="profile.icon != null"
ng-click="aggregate.toggleContentProfileFilter(profile)"
class="toggle-button"
ng-class="{'toggle-button--active': aggregate.isContentProfileFilterActive(profile._id)}"
title="{{ profile.label }}"
aria-label="{{ profile.label }}"
>
<i class="toggle-button__icon icon-{{profile.icon}}" aria-hidden="true"></i>
</a>
</div>

Expand Down
5 changes: 4 additions & 1 deletion scripts/apps/search/components/Associations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ export class Associations extends React.Component<IProps, any> {
onClick={this.openItem}
title={gettext('Associated ') + this.props.item.associations.featuremedia.type}
>
<TypeIcon type={this.props.item.associations.featuremedia.type} />
<TypeIcon
type={this.props.item.associations.featuremedia.type}
contentProfileId={this.props.item.profile}
/>
</div>
);
}
Expand Down
26 changes: 6 additions & 20 deletions scripts/apps/search/components/GridTypeIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import PropTypes from 'prop-types';
import {TypeIcon} from './index';
import classNames from 'classnames';
import {IArticle} from 'superdesk-api';
Expand All @@ -10,25 +9,12 @@ interface IProps {
swimlane?: any;
}

export const GridTypeIcon: React.StatelessComponent<IProps> = (props) => {
if (props.photoGrid) {
return React.createElement('span',
{className: classNames('sd-grid-item__type-icn',
{swimlane: props.swimlane},
)},
React.createElement(TypeIcon, {type: props.item.type}),
);
}
export const GridTypeIcon: React.FunctionComponent<IProps> = (props) => {
const className = props.photoGrid ? classNames('sd-grid-item__type-icn', {swimlane: props.swimlane}) : undefined;

return React.createElement(
'span',
{},
React.createElement(TypeIcon, {type: props.item.type}),
return (
<span className={className}>
<TypeIcon type={props.item.type} contentProfileId={props.item.profile} />
</span>
);
};

GridTypeIcon.propTypes = {
swimlane: PropTypes.any,
item: PropTypes.any,
photoGrid: PropTypes.bool,
};
1 change: 1 addition & 0 deletions scripts/apps/search/components/ListTypeIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export class ListTypeIcon extends React.Component<IProps, IState> {
: (
<TypeIcon
type={this.props.item.type}
contentProfileId={this.props.item.profile}
highlight={this.props.item.highlight}
aria-hidden={true}
/>
Expand Down
23 changes: 22 additions & 1 deletion scripts/apps/search/components/TypeIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React from 'react';
import {gettext} from 'core/utils';
import {dataStore} from 'data-store';

interface IProps {
contentProfileId: string;
type: string;
highlight?: boolean;
'aria-hidden'?: boolean;
Expand All @@ -12,14 +14,31 @@ interface IProps {
*/
export class TypeIcon extends React.PureComponent<IProps> {
render() {
const {type, highlight} = this.props;
const {type, highlight, contentProfileId} = this.props;

if (contentProfileId != null) {
const profile = dataStore.contentProfiles.get(contentProfileId);

if (profile?.icon != null) {
return (
<i
className={'icon-' + profile.icon}
aria-label={gettext('Content profile: {{name}}', {name: profile.label})}
aria-hidden={this.props['aria-hidden'] ?? false}
data-test-id="type-icon"
data-test-value={profile.icon}
/>
);
}
}

if (type === 'composite' && highlight) {
return (
<i
className={'filetype-icon-highlight-pack'}
aria-label={gettext('Article Type {{type}}', {type})}
aria-hidden={this.props['aria-hidden'] ?? false}
data-test-id="type-icon"
/>
);
}
Expand All @@ -30,6 +49,8 @@ export class TypeIcon extends React.PureComponent<IProps> {
title={gettext('Article Type: {{type}}', {type})}
aria-label={gettext('Article Type {{type}}', {type})}
aria-hidden={this.props['aria-hidden'] ?? false}
data-test-id="type-icon"
data-test-value={type}
/>
);
}
Expand Down
3 changes: 2 additions & 1 deletion scripts/apps/search/components/WidgetItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface IProps {
* @description This component is a row in monitoring widget item list.
*/
export class WidgetItem extends React.Component<IProps, any> {
item: any;
item: IArticle;

constructor(props) {
super(props);
Expand Down Expand Up @@ -70,6 +70,7 @@ export class WidgetItem extends React.Component<IProps, any> {
<div className="content-item__type">
<TypeIcon
type={this.item.type}
contentProfileId={this.item.profile}
highlight={this.item.highlight}
/>
</div>
Expand Down
8 changes: 3 additions & 5 deletions scripts/apps/search/components/fields/type.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,15 @@ import {IPropsItemListInfo} from '../ListItemInfo';

class TypeComponent extends React.Component<IPropsItemListInfo> {
render() {
const props = this.props;
const item = this.props.item;

if (props.item.type == null) {
if (item.type == null) {
return null;
}

const {_type, highlight} = props.item;

return (
<span>
<TypeIcon type={_type} highlight={highlight} />
<TypeIcon type={item._type} highlight={item.highlight} contentProfileId={item.profile} />
</span>
);
}
Expand Down
2 changes: 2 additions & 0 deletions scripts/apps/search/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ export const CORE_PROJECTED_FIELDS = {
'translated_from',
'translations',
'schedule_settings',

'profile',
],
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,12 @@ export function ContentProfilesController($scope: IScope, $location, notify, con
.then(this.toggleEdit);
};

this.onIconChange = (val) => {
$scope.editing.form.icon = val;
$scope.ngForm.$dirty = true;
$scope.$apply();
};

/**
* @description Commits the changes made in the editing form for a profile
* to the server.
Expand Down
11 changes: 6 additions & 5 deletions scripts/apps/workspace/content/views/profile-settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@ <h2 class="sd-page__page-heading" translate>Content Profiles</h2>
<ul class="dropdown__menu more-activity-menu pull-right" data-test-id="content-profile-actions--options">
<li><div class="dropdown__menu-label" translate>Actions</div></li>
<li class="dropdown__menu-divider"></li>
<li><button ng-click="ctrl.toggleEdit(type)" title="{{:: 'Edit Content Profile' | translate }}"><i class="icon-pencil"></i>{{:: 'Edit'| translate}}</button></li>
<li><button ng-click="ctrl.toggleEdit(type)" title="{{:: 'Edit Content Profile' | translate }}" aria-label="Edit"><i class="icon-pencil"></i>{{:: 'Edit'| translate}}</button></li>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing gettext in aria-label

<li><button ng-click="ctrl.delete(type)" ng-disabled="type.is_used" title="{{:: type.is_used ? 'Sorry, but profile is in use.' : 'Remove Content Profile' | translate }}"><i class="icon-trash"></i>{{:: 'Remove'| translate}}</button></li>
</ul>
</div>
<div class="card-box__heading sd-d-flex sd-flex-align-items-center">
<i class="icon--white sd-margin-r--1 {{getContentProfileIconByProfileType(type.type)}}"></i>
<i ng-if="type.icon == null" class="icon--white sd-margin-r--1 {{getContentProfileIconByProfileType(type.type)}}"></i>
<i ng-if="type.icon != null" class="icon--white sd-margin-r--1 icon-{{type.icon}}" data-test-id="icon" data-test-value="{{type.icon}}"></i>
{{ type.label}}
</div>
</div>
Expand Down Expand Up @@ -139,12 +140,12 @@ <h3 class="modal__heading">{{ :: 'Editing' | translate }} "{{ editing.form.label
<div class="modal__body sd-padding--0 sd-padding-t--2" data-test-id="content-profile-edit-view">
<form name="editForm">
<fieldset ng-init="setNgForm(editForm)">
<div class="form__row form__row--flex sd-padding-x--2" style="align-items: center">
<div class="form__row form__row--flex sd-padding-x--2" style="align-items: start">
<div class="form__row-item sd-flex-no-grow">
<i class="icon--2x {{getContentProfileIconByProfileType(editing.form.type)}}"></i>
<sd-icon-picker data-value="editing.form.icon" data-on-change="ctrl.onIconChange"></sd-icon-picker>
</div>
<div class="form__row-item">
<div class="sd-line-input sd-line-input--boxed" ng-class="{'sd-line-input--invalid': editForm.label.$error.maxlength}">
<div class="sd-line-input sd-line-input--boxed" style="padding-block-start: 16px;" ng-class="{'sd-line-input--invalid': editForm.label.$error.maxlength}">
<label class="sd-line-input__label" translate>Label</label>
<input type="text" name="label" class="sd-line-input__input" ng-model="editing.form.label" required ng-maxlength="40"/>
<div class="sd-line-input__message" ng-show="editForm.label.$error.maxlength" translate>Please use less than 40 characters</div>
Expand Down
1 change: 1 addition & 0 deletions scripts/core/ArticlesListV2MultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export class MultiSelect extends React.Component<IProps> {
<TypeIcon
type={item.type}
highlight={item.highlight}
contentProfileId={item.profile}
/>
</div>
{checkbox}
Expand Down
3 changes: 2 additions & 1 deletion scripts/core/editor3/directive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ import {
import {fieldsMetaKeys, FIELD_KEY_SEPARATOR, setFieldMetadata} from './helpers/fieldsMeta';
import {AUTHORING_FIELD_PREFERENCES} from 'core/constants';
import {getAutocompleteSuggestions} from 'core/helpers/editor';
import {findParentScope, gettext} from '../utils';
import {gettext} from '../utils';
import {editor3StateToHtml} from './html/to-html/editor3StateToHtml';
import {canAddArticleEmbed} from './components/article-embed/can-add-article-embed';
import {TextStatisticsConnected} from 'apps/authoring/authoring/components/text-statistics-connected';
import {getLabelNameResolver} from 'apps/workspace/helpers/getLabelForFieldId';
import {ValidateCharactersConnected} from 'apps/authoring/authoring/ValidateCharactersConnected';
import {Spacer} from 'core/ui/components/Spacer';
import {copyEmbeddedArticlesIntoAssociations} from 'apps/authoring-react/copy-embedded-articles-into-associations';
import {findParentScope} from 'core/find-parent-scope';

/**
* @ngdoc directive
Expand Down
13 changes: 13 additions & 0 deletions scripts/core/find-parent-scope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {IScope} from 'angular';

export function findParentScope(scope: IScope, predicate: (scope: IScope) => boolean): IScope | null {
let current = scope.$parent;

while (current != null) {
if (predicate(current) === true) {
return current;
} else {
current = current.$parent;
}
}
}
Loading
Loading