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

Custom blocks tab #4519

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions e2e/client/playwright/settings.metadata-editor-block.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {test, expect} from '@playwright/test';
import {restoreDatabaseSnapshot, s} from './utils';
import {treeSelectDriver} from './utils/tree-select-driver';

test('creation and persistance of a custom block', async ({page}) => {
await restoreDatabaseSnapshot();
await page.goto('/#/settings/vocabularies');
await page.locator(s('metadata-navigation')).getByRole('button', {name: 'Custom blocks'}).click();
await page.getByRole('button', {name: 'Add New'}).click();

// Input sample data
await treeSelectDriver('formatting-options').addValue(page, 'h1');
await page.locator(s('vocabulary-edit-content')).getByLabel('Id').fill('test_vocabulary');
await page.locator(s('vocabulary-edit-content')).getByLabel('Name').fill('test_vocabulary');
await page.locator(s('vocabulary-edit-content', 'editor3')).getByRole('textbox').fill('test data');

// Apply formatting option to sample text data
await page.locator(s('editor3')).getByText('test data').click();
await page.locator(s('editor3', 'formatting-option=H1')).click();

// Save editor block
await page.locator(s('vocabulary-edit-footer')).getByRole('button', {name: 'Save'}).click();

// Edit custom block
await page.locator(s('vocabulary-item=test_vocabulary')).hover();
await page.locator(s('vocabulary-item=test_vocabulary', 'vocabulary-item--start-editing')).click();

// Check if formatting option, sample text data
await expect(page.locator(s('editor3', 'formatting-option=H1'))).toBeVisible();
await expect(page.locator(s('editor3')).getByRole('textbox')).toHaveText('test data');
});
45 changes: 45 additions & 0 deletions e2e/client/playwright/utils/tree-select-driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {Page} from '@playwright/test';
import {s} from '.';

export const treeSelectDriver = (dataTestId: string) => {
return {
/**
* Get all available options.
*/
async getValues(page: Page): Promise<Array<string>> {
thecalcc marked this conversation as resolved.
Show resolved Hide resolved
await page.locator(s('open-popover')).click();

return page.locator(s('tree-select-popover', 'options')).getByRole('button').all()
.then((buttons) => Promise.all(buttons.map((button) => button.innerText())));
},

/**
* Add an option/options to the already set options.
*/
async addValue(page: Page, ...options: Array<Array<string> | string>): Promise<void> {
const setOptions = async (options: Array<Array<string> | string>) => {
for (const option of options) {
if (typeof option == 'string') {
await page.locator(s(dataTestId, 'open-popover')).click();
await page.locator(s('tree-select-popover'))
.getByRole('button', {name: new RegExp(option, 'i')})
.click();
} else if (option != null) {
setOptions(option);
}
}
};

await setOptions(options);
},

/**
* Reset already set options and set only the passed one/s.
*/
async setValue(page: Page, ...options: Array<Array<string> | string>) {
await page.locator(s(dataTestId)).getByRole('button', {name: 'remove-sign'}).click();

this.addValue(options);
},
};
};
4 changes: 2 additions & 2 deletions scripts/apps/relations/directives/RelatedItemsDirective.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {gettext} from 'core/utils';
import {AuthoringWorkspaceService} from 'apps/authoring/authoring/services/AuthoringWorkspaceService';
import {IArticle, IVocabulary, IRendition} from 'superdesk-api';
import {IArticle, IRendition, IVocabularyRelatedContent, IVocabularyMedia} from 'superdesk-api';
import {IDirectiveScope} from 'types/Angular/DirectiveScope';
import {getAssociationsByFieldId} from '../../authoring/authoring/controllers/AssociationController';
import {getThumbnailForItem} from 'core/helpers/item';
Expand All @@ -14,7 +14,7 @@ interface IScope extends IDirectiveScope<void> {
relatedItemsNewButton: typeof RelatedItemCreateNewButton;
onCreated: (items: Array<IArticle>) => void;
gettext: (text: any, params?: any) => string;
field: IVocabulary;
field: IVocabularyMedia | IVocabularyRelatedContent;
editable: boolean;
item: IArticle;
loading: boolean;
Expand Down
4 changes: 2 additions & 2 deletions scripts/apps/relations/services/RelationsService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {zipObject} from 'lodash';
import {IArticle, IVocabulary} from 'superdesk-api';
import {IArticle, IVocabularyMedia, IVocabularyRelatedContent} from 'superdesk-api';
import {isPublished, isIngested} from 'apps/archive/utils';
import {gettext} from 'core/utils';

Expand Down Expand Up @@ -79,7 +79,7 @@ export function RelationsService(api, $q) {
})).then((values) => zipObject(relatedItemsKeys, values));
};

this.itemHasAllowedStatus = function(item: IArticle, field: IVocabulary) {
this.itemHasAllowedStatus = function(item: IArticle, field: IVocabularyRelatedContent | IVocabularyMedia) {
return validateWorkflow(item, field?.field_options?.allowed_workflows ?? {}).result;
};
}
13 changes: 12 additions & 1 deletion scripts/apps/vocabularies/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import {gettext} from 'core/utils';

export function getMediaTypes() {
interface IMediaType {
GALLERY: {
id: 'media';
label: string;
};
RELATED_CONTENT: {
id: 'related_content';
label: string;
}
}

export function getMediaTypes(): IMediaType {
return {
GALLERY: {
id: 'media',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {DEFAULT_SCHEMA, getVocabularySelectionTypes, getMediaTypeKeys, getMediaTypes} from '../constants';
import {IVocabulary, IVocabularyItem, IVocabularyTag} from 'superdesk-api';
import {IVocabulary, IVocabularyTag} from 'superdesk-api';
import {IDirectiveScope} from 'types/Angular/DirectiveScope';
import {remove, reduce} from 'lodash';
import {gettext, downloadFile} from 'core/utils';
import {showModal} from '@superdesk/common';
import {UploadConfig} from '../components/UploadConfigModal';
import {EDITOR_BLOCK_FIELD_TYPE} from 'apps/workspace/content/constants';

function getOther() {
return gettext('Other');
Expand Down Expand Up @@ -110,7 +111,7 @@ export function VocabularyConfigController($scope: IScope, $route, $routeParams,
$scope.matchFieldTypeToTab = (tab, fieldType) =>
tab === 'vocabularies' && !fieldType || fieldType &&
(tab === 'text-fields' && fieldType === 'text' ||
tab === 'custom-editor-blocks' && fieldType === 'editor-block' ||
tab === 'custom-editor-blocks' && fieldType === EDITOR_BLOCK_FIELD_TYPE ||
tab === 'date-fields' && fieldType === 'date' ||
tab === 'urls-fields' && fieldType === 'urls' ||
tab === 'related-content-fields' && getMediaTypeKeys().includes(fieldType) ||
Expand Down
135 changes: 119 additions & 16 deletions scripts/apps/vocabularies/controllers/VocabularyEditController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import _ from 'lodash';
import {IVocabularySelectionTypes, getVocabularySelectionTypes, getMediaTypeKeys, getMediaTypes} from '../constants';
import {gettext} from 'core/utils';
import {getFields} from 'apps/fields';
import {IVocabulary} from 'superdesk-api';
import {IArticle, IVocabulary, RICH_FORMATTING_OPTION} from 'superdesk-api';
import {IScope as IScopeConfigController} from './VocabularyConfigController';
import {VocabularyItemsViewEdit} from '../components/VocabularyItemsViewEdit';
import {defaultAllowedWorkflows} from 'apps/relations/services/RelationsService';
import {EDITOR_BLOCK_FIELD_TYPE} from 'apps/workspace/content/constants';
import {getEditor3RichTextFormattingOptions} from 'apps/workspace/content/components/get-content-profiles-form-config';
import {ContentState, convertToRaw} from 'draft-js';
import {getObjectEntriesGeneric} from '../../../core/helpers/utils';

VocabularyEditController.$inject = [
'$scope',
Expand All @@ -32,13 +36,24 @@ interface IScope extends IScopeConfigController {
_errorUniqueness: boolean;
errorMessage: string;
save: () => void;

// custom-editor-block props START
formattingOptionsOnChange?: (options: Array<RICH_FORMATTING_OPTION>) => void;
tomaskikutis marked this conversation as resolved.
Show resolved Hide resolved
editorBlockFormattingOptions?: Array<{value: [RICH_FORMATTING_OPTION, string]}>;
fakeItem?: Partial<IArticle>;
editorBlockFieldId?: string;
// custom-editor-block props END

updateUI: () => void;
requestEditor3DirectivesToGenerateHtml: Array<() => void>;
handleTemplateValueChange: (value: string) => void;
requireAllowedTypesSelection: () => void;
addItem: () => void;
cancel: () => void;
model: any;
schema: any;
schemaFields: Array<any>;
itemsValidation: { valid: boolean };
itemsValidation: {valid: boolean};
customFieldTypes: Array<{id: string, label: string}>;
setCustomFieldConfig: (config: any) => void;
editForm: any;
Expand All @@ -47,6 +62,9 @@ interface IScope extends IScopeConfigController {
}

const idRegex = '^[a-zA-Z0-9-_]+$';
const editorBlockFieldId = 'editor_block_field';

type IFormattingOptionTuple = [notTranslatedOption: RICH_FORMATTING_OPTION, translatedOption: string];

export function VocabularyEditController(
$scope: IScope, notify, api, metadata, cvSchema, relationsService, $timeout,
Expand All @@ -61,16 +79,22 @@ export function VocabularyEditController(
$scope.tab = tab;
};

$scope.requestEditor3DirectivesToGenerateHtml = [];

$scope.idRegex = idRegex;
$scope.selectionTypes = getVocabularySelectionTypes();

if ($scope.matchFieldTypeToTab('related-content-fields', $scope.vocabulary.field_type)) {
if (
$scope.matchFieldTypeToTab('related-content-fields', $scope.vocabulary.field_type)
&& $scope.vocabulary.field_type === 'related_content'
) {
const vocab = $scope.vocabulary;

// Insert default allowed workflows
if ($scope.vocabulary.field_options == null) {
$scope.vocabulary.field_options = {allowed_workflows: defaultAllowedWorkflows};
} else if ($scope.vocabulary.field_options.allowed_workflows == null) {
$scope.vocabulary.field_options.allowed_workflows = defaultAllowedWorkflows;
}
vocab.field_options = {
...(vocab.field_options ?? {}),
allowed_workflows: defaultAllowedWorkflows,
};
}

function onSuccess(result) {
Expand All @@ -84,10 +108,9 @@ export function VocabularyEditController(
function onError(response) {
if (angular.isDefined(response.data._issues)) {
if (angular.isDefined(response.data._issues['validator exception'])) {
notify.error(gettext('Error: ' +
response.data._issues['validator exception']));
notify.error(gettext('Error: {{ message }}', {message: response.data._issues['validator exception']}));
} else if (angular.isDefined(response.data._issues.error) &&
response.data._issues.error.required_field) {
response.data._issues.error.required_field) {
let params = response.data._issues.params;

notify.error(gettext(
Expand All @@ -111,6 +134,67 @@ export function VocabularyEditController(
return true;
}

if ($scope.vocabulary.field_type === EDITOR_BLOCK_FIELD_TYPE) {
const vocabulary = $scope.vocabulary;

$scope.editorBlockFieldId = editorBlockFieldId;

$scope.fakeItem = {
fields_meta: {

/**
* Fake field, needed for compatibility with sdEditor3 directive
*/
[editorBlockFieldId]: {
draftjsState: vocabulary.field_options?.template != null
? vocabulary.field_options.template
: [convertToRaw(ContentState.createFromText(''))],
},
},
};

$scope.editorBlockFormattingOptions = (() => {
const excludedOptions: Set<RICH_FORMATTING_OPTION> = new Set<RICH_FORMATTING_OPTION>([
'multi-line quote',
'comments',
'annotation',
'suggestions',
'table',
'media',
]);

const formattingOptions = getObjectEntriesGeneric<RICH_FORMATTING_OPTION, string>(
getEditor3RichTextFormattingOptions(),
)
.filter(([notTranslatedOption]) =>
excludedOptions.has(notTranslatedOption) === false,
)
.map(([notTranslatedOption, translatedOption]) =>
({value: [notTranslatedOption, translatedOption]}),
);

return formattingOptions as Array<{value: IFormattingOptionTuple}>;
})();

$scope.formattingOptionsOnChange = function(options) {
if (vocabulary.field_options == null) {
vocabulary.field_options = {};
}

vocabulary.field_options.formatting_options = options;

/**
* Apply current changes to item and save them to the field as html,
* so when formatting options are updated editor sill has the changes
*/
$scope.requestEditor3DirectivesToGenerateHtml.forEach((fn) => {
fn();
});

$scope.updateUI();
};
}

/**
* Save current edit modal contents on backend.
*/
Expand All @@ -120,6 +204,22 @@ export function VocabularyEditController(
$scope.errorMessage = null;
delete $scope.vocabulary['_deleted'];

if ($scope.vocabulary.field_type === EDITOR_BLOCK_FIELD_TYPE) {
$scope.requestEditor3DirectivesToGenerateHtml.forEach((fn) => {
fn();
});
$scope.vocabulary.field_options = $scope.vocabulary.field_options ?? {};

/**
* Formatting options are updated in formattingOptionsOnChange and
* don't need to be updated explicitly here again.
*/
$scope.vocabulary.field_options = {
...$scope.vocabulary.field_options,
template: $scope.fakeItem.fields_meta?.[editorBlockFieldId].draftjsState,
};
}

if ($scope.vocabulary._id === 'crop_sizes') {
var activeItems = _.filter($scope.vocabulary.items, (o) => o.is_active);

Expand Down Expand Up @@ -175,7 +275,7 @@ export function VocabularyEditController(
return false;
}

if ($scope.vocabulary.field_options == null || $scope.vocabulary.field_options.allowed_types == null) {
if ($scope.vocabulary.field_type !== 'related_content') {
tomaskikutis marked this conversation as resolved.
Show resolved Hide resolved
return true;
}

Expand Down Expand Up @@ -231,12 +331,16 @@ export function VocabularyEditController(
label: fields[id].label,
}));

$scope.setCustomFieldConfig = (config) => {
$scope.vocabulary.custom_field_config = config;
$scope.updateUI = () => {
$scope.editForm.$setDirty();
$scope.$applyAsync();
};

$scope.setCustomFieldConfig = (config) => {
$scope.vocabulary.custom_field_config = config;
$scope.updateUI();
};

let placeholderElement = null;

// wait for the template to render to the placeholder element is available
Expand All @@ -252,8 +356,7 @@ export function VocabularyEditController(
schemaFields={$scope.schemaFields}
newItemTemplate={{...$scope.model, is_active: true}}
setDirty={() => {
$scope.editForm.$setDirty();
$scope.$apply();
$scope.updateUI();
}}
setItemsValid={(valid) => {
$scope.itemsValidation.valid = valid;
Expand Down
4 changes: 2 additions & 2 deletions scripts/apps/vocabularies/views/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
<h2 class="sd-page__page-heading" translate>Metadata management</h2>
<span class="sd-page__element-grow"></span>
<button class="btn btn--hollow btn--primary"
ng-click="uploadConfig()" ng-if="tab === 'vocabularies'">
ng-click="uploadConfig()" ng-if="tab === 'vocabularies' || tab === 'custom-editor-blocks'">
<i class="icon-upload icon--blue"></i>
<span translate>Upload Config</span>
</button>
</div>

<div class="sd-page__header sd-page__header--white">
<div data-test-id="metadata-navigation" class="sd-page__header sd-page__header--white">
<ul class="nav nav-tabs">
<li ng-class="{active: tab === 'vocabularies'}">
<button ng-click="tab = 'vocabularies'" translate>Vocabularies</button>
Expand Down
Loading
Loading