Skip to content

Commit 9e639d0

Browse files
Merge pull request #1 from flotiq/feature/25125-kaban-view-ui-plugin
#25125 kaban view UI plugin
2 parents b271766 + 988aa8d commit 9e639d0

26 files changed

+3121
-1483
lines changed

.docs/images/settings-screen.png

58.3 KB
Loading

.prettierrc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"endOfLine": "lf",
3+
"semi": true,
4+
"singleQuote": true,
5+
"tabWidth": 2,
6+
"trailingComma": "all"
7+
}

README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@
1010
6. paste js code from `./build/static/js/main.xxxxxxxx.js` to Flotiq console
1111
7. navigate to affected Flotiq pages
1212

13+
## Usage
14+
15+
### Overview
16+
17+
This plugin transforms the default Flotiq `content-type-objects` page into a `Kanban board`, where content objects are
18+
displayed as cards within their respective columns.
19+
20+
### Configuration
21+
22+
The plugin requires you to select a `Content type definition` that will be converted into a `Kanban board`. You also
23+
need to choose a field of type `Select` or `Radio`, which will determine the Columns on the board. Additionally, select
24+
a `text`field that will serve as the `title` displayed on `each card`.
25+
26+
![](.docs/images/settings-screen.png)
27+
28+
Additionally, the plugin lets you configure optional fields to be displayed on the card, such as an `image` or up to
29+
three `additional fields` shown at the `bottom of the card`. supported types for
30+
`additional fields`: `text, number, select, date, checkbox, radio`
1331

1432
## Deployment
1533

@@ -31,11 +49,12 @@
3149

3250
1. Open Flotiq editor
3351
2. Open Chrome Dev console
34-
3. Paste the content of `static/js/main.xxxxxxxx.js`
52+
3. Paste the content of `static/js/main.xxxxxxxx.js`
3553
4. Navigate to the view that is modified by the plugin
3654

3755
### Deployment
3856

3957
1. Open Flotiq editor
40-
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)
58+
2. Add a new plugin and paste the URL to the hosted `plugin-manifest.json` file (you can
59+
use `https://localhost:3050/plugin-manifest.json` as long as you have accepted self-signed certificate for this url)
4160
3. Navigate to the view that is modified by the plugin

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33
"version": "0.1.0",
44
"private": true,
55
"dependencies": {
6+
"@dnd-kit/core": "^6.1.0",
7+
"@dnd-kit/sortable": "^8.0.0",
8+
"@dnd-kit/utilities": "^3.2.2",
69
"@testing-library/jest-dom": "^5.17.0",
710
"@testing-library/react": "^13.4.0",
811
"@testing-library/user-event": "^13.5.0",
12+
"i18next": "^23.11.5",
13+
"raw-loader": "^4.0.2",
914
"react": "^18.2.0",
1015
"react-dom": "^18.2.0",
1116
"react-scripts": "5.0.1",
@@ -38,6 +43,7 @@
3843
},
3944
"devDependencies": {
4045
"concurrently": "^8.2.2",
41-
"cpx": "^1.5.0"
46+
"cpx": "^1.5.0",
47+
"prettier": "^3.1.1"
4248
}
4349
}

src/ShinyRow.js

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/plugin-helpers.js renamed to src/common/plugin-helpers.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,18 @@ export const addElementToCache = (element, root, key) => {
66
root,
77
};
88

9-
element.addEventListener("flotiq.detached", () => {
10-
setTimeout(() => {
11-
return delete appRoots[key];
9+
let detachTimeoutId;
10+
11+
element.addEventListener('flotiq.attached', () => {
12+
if (detachTimeoutId) {
13+
clearTimeout(detachTimeoutId);
14+
detachTimeoutId = null;
15+
}
16+
});
17+
18+
element.addEventListener('flotiq.detached', () => {
19+
detachTimeoutId = setTimeout(() => {
20+
delete appRoots[key];
1221
}, 50);
1322
});
1423
};
@@ -25,3 +34,11 @@ export const registerFn = (pluginInfo, callback) => {
2534
if (!window.initFlotiqPlugins) window.initFlotiqPlugins = [];
2635
window.initFlotiqPlugins.push({ pluginInfo, callback });
2736
};
37+
38+
export const addObjectToCache = (key, data = {}) => {
39+
appRoots[key] = data;
40+
};
41+
42+
export const removeRoot = (key) => {
43+
delete appRoots[key];
44+
};

src/common/valid-fields.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import pluginInfo from '../plugin-manifest.json';
2+
3+
export const validSourceFields = ['select', 'radio'];
4+
5+
export const validCardTitleFields = ['text'];
6+
7+
export const validCardImageFields = ['datasource'];
8+
9+
export const validCardAdditionalFields = [
10+
'text',
11+
'number',
12+
'select',
13+
'date',
14+
'checkbox',
15+
'radio',
16+
];
17+
18+
export const getValidFields = (contentTypes) => {
19+
const sourceFields = {};
20+
const sourceFieldsKeys = {};
21+
22+
const cardTitleFields = {};
23+
const cardTitleFieldsKeys = {};
24+
25+
const cardImageFields = {};
26+
const cardImageFieldsKeys = {};
27+
28+
const cardAdditionalFields = {};
29+
const cardAdditionalFieldsKeys = {};
30+
31+
contentTypes
32+
?.filter(({ internal }) => !internal)
33+
?.map(({ name, label }) => ({ value: name, label }));
34+
35+
(contentTypes || []).forEach(({ name, metaDefinition }) => {
36+
sourceFields[name] = [];
37+
sourceFieldsKeys[name] = [];
38+
39+
cardTitleFields[name] = [];
40+
cardTitleFieldsKeys[name] = [];
41+
42+
cardImageFields[name] = [];
43+
cardImageFieldsKeys[name] = [];
44+
45+
cardAdditionalFields[name] = [];
46+
cardAdditionalFieldsKeys[name] = [];
47+
48+
Object.entries(metaDefinition?.propertiesConfig || {}).forEach(
49+
([key, fieldConfig]) => {
50+
const inputType = fieldConfig?.inputType;
51+
52+
if (validSourceFields?.includes(inputType)) {
53+
sourceFields[name].push({ value: key, label: fieldConfig.label });
54+
sourceFieldsKeys[name].push(key);
55+
}
56+
57+
if (validCardTitleFields?.includes(inputType)) {
58+
cardTitleFields[name].push({
59+
value: key,
60+
label: fieldConfig.label,
61+
});
62+
cardTitleFieldsKeys[name].push(key);
63+
}
64+
65+
if (
66+
validCardImageFields?.includes(inputType) &&
67+
fieldConfig?.validation?.relationContenttype === '_media'
68+
) {
69+
cardImageFields[name].push({
70+
value: key,
71+
label: fieldConfig.label,
72+
});
73+
cardImageFieldsKeys[name].push(key);
74+
}
75+
76+
if (validCardAdditionalFields?.includes(inputType)) {
77+
cardAdditionalFields[name].push({
78+
value: key,
79+
label: fieldConfig.label,
80+
});
81+
cardAdditionalFieldsKeys[name].push(key);
82+
}
83+
},
84+
);
85+
});
86+
87+
return {
88+
sourceFields,
89+
sourceFieldsKeys,
90+
cardTitleFields,
91+
cardTitleFieldsKeys,
92+
cardImageFields,
93+
cardImageFieldsKeys,
94+
cardAdditionalFields,
95+
cardAdditionalFieldsKeys,
96+
};
97+
};
98+
99+
export const validFieldsCacheKey = `${pluginInfo.id}-form-valid-fields`;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { getCachedElement } from '../../common/plugin-helpers';
2+
import {
3+
validCardAdditionalFields,
4+
validCardTitleFields,
5+
validFieldsCacheKey,
6+
validSourceFields,
7+
} from '../../common/valid-fields';
8+
import i18n from '../../i18n';
9+
10+
const insertSelectOptions = (config, options = [], emptyOptionMessage) => {
11+
config.additionalHelpTextClasses = 'break-normal';
12+
13+
if (options.length === 0) {
14+
config.options = [
15+
{ value: 'empty', label: emptyOptionMessage, disabled: true },
16+
];
17+
return;
18+
}
19+
config.options = options;
20+
};
21+
22+
export const handlePluginFormConfig = ({ name, config, formik }) => {
23+
const { index, type } =
24+
name.match(/kanbanBoard\[(?<index>\d+)\].(?<type>\w+)/)?.groups || {};
25+
26+
if (index == null || !type) return;
27+
const ctd = formik.values.kanbanBoard[index].content_type;
28+
const {
29+
sourceFields,
30+
cardTitleFields,
31+
cardImageFields,
32+
cardAdditionalFields,
33+
} = getCachedElement(validFieldsCacheKey);
34+
35+
const keysToClearOnCtdChange = [
36+
'source',
37+
'title',
38+
'image',
39+
'additional_fields',
40+
];
41+
42+
switch (type) {
43+
case 'content_type':
44+
config.onChange = (_, value) => {
45+
if (value == null) formik.setFieldValue(name, '');
46+
else formik.setFieldValue(name, value);
47+
48+
keysToClearOnCtdChange.forEach((key) => {
49+
formik.setFieldValue(`kanbanBoard[${index}].${key}`, '');
50+
});
51+
};
52+
break;
53+
case 'source':
54+
insertSelectOptions(
55+
config,
56+
sourceFields?.[ctd],
57+
i18n.t('NonRequiredFieldsInCTD', {
58+
types: validSourceFields.join(', '),
59+
}),
60+
);
61+
break;
62+
case 'title':
63+
insertSelectOptions(
64+
config,
65+
cardTitleFields?.[ctd],
66+
i18n.t('NonRequiredFieldsInCTD', {
67+
types: validCardTitleFields.join(', '),
68+
}),
69+
);
70+
break;
71+
case 'image':
72+
insertSelectOptions(
73+
config,
74+
cardImageFields?.[ctd],
75+
i18n.t('NonRequiredFieldsInCTD', {
76+
types: ['Relation to media, media'],
77+
}),
78+
);
79+
break;
80+
case 'additional_fields':
81+
insertSelectOptions(
82+
config,
83+
cardAdditionalFields?.[ctd],
84+
i18n.t('NonRequiredFieldsInCTD', {
85+
types: validCardAdditionalFields.join(', '),
86+
}),
87+
);
88+
break;
89+
default:
90+
break;
91+
}
92+
};

src/i18n.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import i18n from 'i18next';
2+
3+
i18n.init({
4+
fallbackLng: 'en',
5+
supportedLngs: ['en', 'pl'],
6+
resources: {
7+
en: {
8+
translation: {
9+
Source: 'Column field name',
10+
SourceHelpText:
11+
'Pick the field which will be used to organize cards in columns, each possible value -> new column. Allowed types: {{types}}',
12+
ContentType: 'Content Type',
13+
ContentTypeHelpText: '',
14+
Title: 'Title',
15+
TitleHelpText:
16+
'Pick the field which will be used to display title in card preview. Allowed types: {{types}}',
17+
Image: 'Image',
18+
ImageHelpText:
19+
'Pick the field which will be used to display image in card preview (optional). Allowed types: {{types}}',
20+
AdditionalFields: 'Additional Fields',
21+
AdditionalFieldsHelpText:
22+
'Pick the fields which will be used to display additional fields in card preview (optional). Allowed types: {{types}}',
23+
FieldRequired: 'Field is required',
24+
WrongFieldType: 'This field type is not supported',
25+
CardDelete: 'Content objects deleted (1)',
26+
FetchError:
27+
'Error occurred while connecting to the server, please try again later.',
28+
NonRequiredFieldsInCTD:
29+
'Make sure the selected content type contains fields that can be used in the plugin. Allowed types: {{types}}',
30+
},
31+
},
32+
pl: {
33+
translation: {
34+
Source: 'Pole kolumny',
35+
SourceHelpText:
36+
'Wybierz pole, które będzie użyte do organizowania kart w kolumnach, każda możliwa wartość -> nowa kolumna. Dozwolone typy: {{types}}',
37+
ContentType: 'Typ zawartości',
38+
ContentTypeHelpText: '',
39+
Title: 'Tytuł',
40+
TitleHelpText:
41+
'Wybierz pole, które będzie użyte do wyświetlania tytułu w podglądzie karty. Dozwolone typy: {{types}}',
42+
Image: 'Obraz',
43+
ImageHelpText:
44+
'Wybierz pole, które będzie użyte do wyświetlania obrazu w podglądzie karty (opcjonalne). Dozwolone typy: {{types}}',
45+
AdditionalFields: 'Dodatkowe Pole 1',
46+
AdditionalFieldsHelpText:
47+
'Wybierz pola, które będą użyte do wyświetlania dodatkowych pól w podglądzie karty (opcjonalne). Dozwolone typy: {{types}}',
48+
FieldRequired: 'Pole jest wymagane',
49+
WrongFieldType: 'Ten typ pola nie jest wspierany',
50+
CardDelete: 'Usunięto obiekty (1)',
51+
FetchError:
52+
'Wystąpił błąd połączenia z serwerem, spróbuj ponownie później.',
53+
NonRequiredFieldsInCTD:
54+
'pewnij się, że wybrany typ definicji zawiera pola, które mogą być wykorzystane we wtyczce. Dozwolone typy: {{types}}',
55+
},
56+
},
57+
},
58+
});
59+
60+
export default i18n;

0 commit comments

Comments
 (0)