Skip to content

Commit

Permalink
Merge pull request #1 from flotiq/feature/25125-kaban-view-ui-plugin
Browse files Browse the repository at this point in the history
#25125 kaban view UI plugin
  • Loading branch information
KrzysztofBrylski authored Aug 11, 2024
2 parents b271766 + 988aa8d commit 9e639d0
Show file tree
Hide file tree
Showing 26 changed files with 3,121 additions and 1,483 deletions.
Binary file added .docs/images/settings-screen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"endOfLine": "lf",
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all"
}
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@
6. paste js code from `./build/static/js/main.xxxxxxxx.js` to Flotiq console
7. navigate to affected Flotiq pages

## Usage

### Overview

This plugin transforms the default Flotiq `content-type-objects` page into a `Kanban board`, where content objects are
displayed as cards within their respective columns.

### Configuration

The plugin requires you to select a `Content type definition` that will be converted into a `Kanban board`. You also
need to choose a field of type `Select` or `Radio`, which will determine the Columns on the board. Additionally, select
a `text`field that will serve as the `title` displayed on `each card`.

![](.docs/images/settings-screen.png)

Additionally, the plugin lets you configure optional fields to be displayed on the card, such as an `image` or up to
three `additional fields` shown at the `bottom of the card`. supported types for
`additional fields`: `text, number, select, date, checkbox, radio`

## Deployment

Expand All @@ -31,11 +49,12 @@

1. Open Flotiq editor
2. Open Chrome Dev console
3. Paste the content of `static/js/main.xxxxxxxx.js`
3. Paste the content of `static/js/main.xxxxxxxx.js`
4. Navigate to the view that is modified by the plugin

### Deployment

1. Open Flotiq editor
2. Add a new plugin and paste the URL to the hosted `plugin-manifest.json` file (you can use `https://localhost:3050/plugin-manifest.json` as long as you have accepted self-signed certificate for this url)
2. Add a new plugin and paste the URL to the hosted `plugin-manifest.json` file (you can
use `https://localhost:3050/plugin-manifest.json` as long as you have accepted self-signed certificate for this url)
3. Navigate to the view that is modified by the plugin
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"i18next": "^23.11.5",
"raw-loader": "^4.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
Expand Down Expand Up @@ -38,6 +43,7 @@
},
"devDependencies": {
"concurrently": "^8.2.2",
"cpx": "^1.5.0"
"cpx": "^1.5.0",
"prettier": "^3.1.1"
}
}
28 changes: 0 additions & 28 deletions src/ShinyRow.js

This file was deleted.

23 changes: 20 additions & 3 deletions src/plugin-helpers.js → src/common/plugin-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,18 @@ export const addElementToCache = (element, root, key) => {
root,
};

element.addEventListener("flotiq.detached", () => {
setTimeout(() => {
return delete appRoots[key];
let detachTimeoutId;

element.addEventListener('flotiq.attached', () => {
if (detachTimeoutId) {
clearTimeout(detachTimeoutId);
detachTimeoutId = null;
}
});

element.addEventListener('flotiq.detached', () => {
detachTimeoutId = setTimeout(() => {
delete appRoots[key];
}, 50);
});
};
Expand All @@ -25,3 +34,11 @@ export const registerFn = (pluginInfo, callback) => {
if (!window.initFlotiqPlugins) window.initFlotiqPlugins = [];
window.initFlotiqPlugins.push({ pluginInfo, callback });
};

export const addObjectToCache = (key, data = {}) => {
appRoots[key] = data;
};

export const removeRoot = (key) => {
delete appRoots[key];
};
99 changes: 99 additions & 0 deletions src/common/valid-fields.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import pluginInfo from '../plugin-manifest.json';

export const validSourceFields = ['select', 'radio'];

export const validCardTitleFields = ['text'];

export const validCardImageFields = ['datasource'];

export const validCardAdditionalFields = [
'text',
'number',
'select',
'date',
'checkbox',
'radio',
];

export const getValidFields = (contentTypes) => {
const sourceFields = {};
const sourceFieldsKeys = {};

const cardTitleFields = {};
const cardTitleFieldsKeys = {};

const cardImageFields = {};
const cardImageFieldsKeys = {};

const cardAdditionalFields = {};
const cardAdditionalFieldsKeys = {};

contentTypes
?.filter(({ internal }) => !internal)
?.map(({ name, label }) => ({ value: name, label }));

(contentTypes || []).forEach(({ name, metaDefinition }) => {
sourceFields[name] = [];
sourceFieldsKeys[name] = [];

cardTitleFields[name] = [];
cardTitleFieldsKeys[name] = [];

cardImageFields[name] = [];
cardImageFieldsKeys[name] = [];

cardAdditionalFields[name] = [];
cardAdditionalFieldsKeys[name] = [];

Object.entries(metaDefinition?.propertiesConfig || {}).forEach(
([key, fieldConfig]) => {
const inputType = fieldConfig?.inputType;

if (validSourceFields?.includes(inputType)) {
sourceFields[name].push({ value: key, label: fieldConfig.label });
sourceFieldsKeys[name].push(key);
}

if (validCardTitleFields?.includes(inputType)) {
cardTitleFields[name].push({
value: key,
label: fieldConfig.label,
});
cardTitleFieldsKeys[name].push(key);
}

if (
validCardImageFields?.includes(inputType) &&
fieldConfig?.validation?.relationContenttype === '_media'
) {
cardImageFields[name].push({
value: key,
label: fieldConfig.label,
});
cardImageFieldsKeys[name].push(key);
}

if (validCardAdditionalFields?.includes(inputType)) {
cardAdditionalFields[name].push({
value: key,
label: fieldConfig.label,
});
cardAdditionalFieldsKeys[name].push(key);
}
},
);
});

return {
sourceFields,
sourceFieldsKeys,
cardTitleFields,
cardTitleFieldsKeys,
cardImageFields,
cardImageFieldsKeys,
cardAdditionalFields,
cardAdditionalFieldsKeys,
};
};

export const validFieldsCacheKey = `${pluginInfo.id}-form-valid-fields`;
92 changes: 92 additions & 0 deletions src/field-config/plugin-form/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { getCachedElement } from '../../common/plugin-helpers';
import {
validCardAdditionalFields,
validCardTitleFields,
validFieldsCacheKey,
validSourceFields,
} from '../../common/valid-fields';
import i18n from '../../i18n';

const insertSelectOptions = (config, options = [], emptyOptionMessage) => {
config.additionalHelpTextClasses = 'break-normal';

if (options.length === 0) {
config.options = [
{ value: 'empty', label: emptyOptionMessage, disabled: true },
];
return;
}
config.options = options;
};

export const handlePluginFormConfig = ({ name, config, formik }) => {
const { index, type } =
name.match(/kanbanBoard\[(?<index>\d+)\].(?<type>\w+)/)?.groups || {};

if (index == null || !type) return;
const ctd = formik.values.kanbanBoard[index].content_type;
const {
sourceFields,
cardTitleFields,
cardImageFields,
cardAdditionalFields,
} = getCachedElement(validFieldsCacheKey);

const keysToClearOnCtdChange = [
'source',
'title',
'image',
'additional_fields',
];

switch (type) {
case 'content_type':
config.onChange = (_, value) => {
if (value == null) formik.setFieldValue(name, '');
else formik.setFieldValue(name, value);

keysToClearOnCtdChange.forEach((key) => {
formik.setFieldValue(`kanbanBoard[${index}].${key}`, '');
});
};
break;
case 'source':
insertSelectOptions(
config,
sourceFields?.[ctd],
i18n.t('NonRequiredFieldsInCTD', {
types: validSourceFields.join(', '),
}),
);
break;
case 'title':
insertSelectOptions(
config,
cardTitleFields?.[ctd],
i18n.t('NonRequiredFieldsInCTD', {
types: validCardTitleFields.join(', '),
}),
);
break;
case 'image':
insertSelectOptions(
config,
cardImageFields?.[ctd],
i18n.t('NonRequiredFieldsInCTD', {
types: ['Relation to media, media'],
}),
);
break;
case 'additional_fields':
insertSelectOptions(
config,
cardAdditionalFields?.[ctd],
i18n.t('NonRequiredFieldsInCTD', {
types: validCardAdditionalFields.join(', '),
}),
);
break;
default:
break;
}
};
60 changes: 60 additions & 0 deletions src/i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import i18n from 'i18next';

i18n.init({
fallbackLng: 'en',
supportedLngs: ['en', 'pl'],
resources: {
en: {
translation: {
Source: 'Column field name',
SourceHelpText:
'Pick the field which will be used to organize cards in columns, each possible value -> new column. Allowed types: {{types}}',
ContentType: 'Content Type',
ContentTypeHelpText: '',
Title: 'Title',
TitleHelpText:
'Pick the field which will be used to display title in card preview. Allowed types: {{types}}',
Image: 'Image',
ImageHelpText:
'Pick the field which will be used to display image in card preview (optional). Allowed types: {{types}}',
AdditionalFields: 'Additional Fields',
AdditionalFieldsHelpText:
'Pick the fields which will be used to display additional fields in card preview (optional). Allowed types: {{types}}',
FieldRequired: 'Field is required',
WrongFieldType: 'This field type is not supported',
CardDelete: 'Content objects deleted (1)',
FetchError:
'Error occurred while connecting to the server, please try again later.',
NonRequiredFieldsInCTD:
'Make sure the selected content type contains fields that can be used in the plugin. Allowed types: {{types}}',
},
},
pl: {
translation: {
Source: 'Pole kolumny',
SourceHelpText:
'Wybierz pole, które będzie użyte do organizowania kart w kolumnach, każda możliwa wartość -> nowa kolumna. Dozwolone typy: {{types}}',
ContentType: 'Typ zawartości',
ContentTypeHelpText: '',
Title: 'Tytuł',
TitleHelpText:
'Wybierz pole, które będzie użyte do wyświetlania tytułu w podglądzie karty. Dozwolone typy: {{types}}',
Image: 'Obraz',
ImageHelpText:
'Wybierz pole, które będzie użyte do wyświetlania obrazu w podglądzie karty (opcjonalne). Dozwolone typy: {{types}}',
AdditionalFields: 'Dodatkowe Pole 1',
AdditionalFieldsHelpText:
'Wybierz pola, które będą użyte do wyświetlania dodatkowych pól w podglądzie karty (opcjonalne). Dozwolone typy: {{types}}',
FieldRequired: 'Pole jest wymagane',
WrongFieldType: 'Ten typ pola nie jest wspierany',
CardDelete: 'Usunięto obiekty (1)',
FetchError:
'Wystąpił błąd połączenia z serwerem, spróbuj ponownie później.',
NonRequiredFieldsInCTD:
'pewnij się, że wybrany typ definicji zawiera pola, które mogą być wykorzystane we wtyczce. Dozwolone typy: {{types}}',
},
},
},
});

export default i18n;
Loading

0 comments on commit 9e639d0

Please sign in to comment.