To receive notifications about scheduled maintenance, please subscribe to the mailing-list gitlab-operations@sympa.ethz.ch. You can subscribe to the mailing-list at https://sympa.ethz.ch

Commit 36dcc986 authored by Hermann's avatar Hermann Committed by Sandro Lutz

Updating admintools to make use of new web-ui-components repository

parent f78eb60a
......@@ -18,6 +18,7 @@
"@material/drawer": "^0.30.0",
"@material/select": "^0.35.1",
"ajv": "^5.5.0",
"amiv-web-ui-components": "git+https://git@gitlab.ethz.ch/amiv/web-ui-components.git",
"axios": "^0.17.1",
"client-oauth2": "^4.2.0",
"mithril": "^1.1.6",
......@@ -32,7 +33,7 @@
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-preset-env": "^1.7.0",
"compression-webpack-plugin": "^1.1.11",
"css-loader": "^0.28.11",
"eslint": "^4.10.0",
......@@ -41,7 +42,7 @@
"eslint-plugin-import": "^2.9.0",
"file-loader": "^1.1.5",
"style-loader": "^0.19.0",
"webpack": "^3.8.1",
"webpack": "^3.12.0",
"webpack-dev-server": "^2.9.5"
}
}
This diff is collapsed.
import m from 'mithril';
import { DatalistController } from 'amiv-web-ui-components';
import { events as config } from '../resourceConfig.json';
import TableView from '../views/tableView';
import DatalistController from '../listcontroller';
import { dateFormatter } from '../utils';
import { ResourceHandler } from '../auth';
/* Table of all Events
......@@ -13,7 +14,8 @@ import { dateFormatter } from '../utils';
export default class EventTable {
constructor() {
this.ctrl = new DatalistController('events', {}, config.tableKeys);
this.handler = new ResourceHandler('events', config.tableKeys);
this.ctrl = new DatalistController((query, search) => this.handler.get({ search, ...query }));
}
getItemData(data) {
......
......@@ -7,6 +7,7 @@ import {
TextField,
} from 'polythene-mithril';
import { styler } from 'polythene-core-css';
import { DropdownCard } from 'amiv-web-ui-components';
// eslint-disable-next-line import/extensions
import { apiUrl } from 'networkConfig';
import ItemView from '../views/itemView';
......@@ -14,7 +15,7 @@ import { eventsignups as signupConfig } from '../resourceConfig.json';
import TableView from '../views/tableView';
import RelationlistController from '../relationlistcontroller';
import { dateFormatter } from '../utils';
import { icons, DropdownCard, Property, chip } from '../views/elements';
import { icons, Property, chip } from '../views/elements';
import { ResourceHandler } from '../auth';
const viewLayout = [
......
import m from 'mithril';
import { TextField } from 'polythene-mithril';
import { ListSelect, DatalistController } from 'amiv-web-ui-components';
// eslint-disable-next-line import/extensions
import { apiUrl } from 'networkConfig';
import SelectList from '../views/selectList';
import { ResourceHandler } from '../auth';
import { MDCSelect } from '../views/selectOption';
import DatalistController from '../listcontroller';
import EditView from '../views/editView';
......@@ -78,23 +78,21 @@ class PermissionEditor {
export default class NewGroup extends EditView {
constructor(vnode) {
super(vnode);
this.userController = new DatalistController(
'users', {},
['firstname', 'lastname', 'email', 'nethz'],
);
console.log(this.data);
this.userHandler = new ResourceHandler('users', ['firstname', 'lastname', 'email', 'nethz']);
this.userController = new DatalistController((query, search) =>
this.userHandler.get({ search, ...query }));
}
beforeSubmit() {
// exchange moderator object with string of id
const { moderator } = this.data;
if (moderator) { this.data.moderator = `${moderator._id}`; }
const { moderator } = this.form.data;
if (moderator) { this.form.data.moderator = `${moderator._id}`; }
this.submit();
}
view() {
return this.layout([
...this.renderPage({
...this.form.renderPage({
name: { type: 'text', label: 'Group Name' },
allow_self_enrollment: {
type: 'checkbox',
......@@ -107,17 +105,17 @@ export default class NewGroup extends EditView {
}),
m('div', { style: { display: 'flex' } }, [
m(TextField, { label: 'Group Moderator: ', disabled: true, style: { width: '160px' } }),
m('div', { style: { 'flex-grow': 1 } }, m(SelectList, {
m('div', { style: { 'flex-grow': 1 } }, m(ListSelect, {
controller: this.userController,
selection: this.data.moderator,
selection: this.form.data.moderator,
listTileAttrs: user => Object.assign({}, { title: `${user.firstname} ${user.lastname}` }),
selectedText: user => `${user.firstname} ${user.lastname}`,
onSelect: (data) => { this.data.moderator = data; },
onSelect: (data) => { console.log('data'); this.form.data.moderator = data; },
})),
]),
m(PermissionEditor, {
permissions: this.data.permissions,
onChange: (newPermissions) => { this.data.permissions = newPermissions; },
permissions: this.form.data.permissions,
onChange: (newPermissions) => { this.form.data.permissions = newPermissions; },
}),
]);
}
......
import m from 'mithril';
import { Card } from 'polythene-mithril';
import DatalistController from '../listcontroller';
import { DatalistController } from 'amiv-web-ui-components';
import { loadingScreen } from '../layout';
import { ResourceHandler } from '../auth';
class GroupListItem {
......@@ -18,7 +19,11 @@ class GroupListItem {
export default class GroupList {
constructor() {
this.ctrl = new DatalistController('groups', { sort: [['name', 1]] }, ['name']);
this.handler = new ResourceHandler('groups', ['name']);
this.ctrl = new DatalistController(
(query, search) => this.handler.get({ search, ...query }),
{ sort: [['name', 1]] },
);
this.data = [];
this.ctrl.getFullList().then((list) => { this.data = list; m.redraw(); });
}
......
......@@ -7,14 +7,13 @@ import {
TextField,
Icon,
} from 'polythene-mithril';
import { icons, Property, DropdownCard, chip } from '../views/elements';
import { DatalistController, ListSelect, DropdownCard } from 'amiv-web-ui-components';
import { icons, Property, chip } from '../views/elements';
import { colors } from '../style';
import ItemView from '../views/itemView';
import TableView from '../views/tableView';
import DatalistController from '../listcontroller';
import RelationlistController from '../relationlistcontroller';
import SelectList from '../views/selectList';
import { ResourceHandler } from '../auth';
......@@ -26,10 +25,9 @@ class MembersTable {
this.ctrl = new RelationlistController('groupmemberships', 'users', { where: { group } });
// true while in the modus of adding a member
this.addmode = false;
this.userController = new DatalistController(
'users', {},
['firstname', 'lastname', 'email', 'nethz'],
);
this.userHandler = new ResourceHandler('users');
this.userController = new DatalistController((query, search) =>
this.userHandler.get({ search, ...query }));
}
itemRow(data) {
......@@ -59,7 +57,7 @@ class MembersTable {
return m(Card, {
style: { height: '500px' },
content: m('div', [
this.addmode ? m(SelectList, {
this.addmode ? m(ListSelect, {
controller: this.userController,
listTileAttrs: user => Object.assign({}, { title: `${user.firstname} ${user.lastname}` }),
selectedText: user => `${user.firstname} ${user.lastname}`,
......@@ -181,7 +179,6 @@ class EmailTable {
}
export default class viewGroup extends ItemView {
oninit() {
// load the number of members in this group
const handler = new ResourceHandler('groupmemberships');
......
......@@ -6,7 +6,7 @@ export default class newJob extends EditView {
view() {
return this.layout([
m('h3', 'Add a New Job Offer'),
...this.renderPage({
...this.form.renderPage({
title_de: { type: 'text', label: 'German Title' },
}),
]);
......
import m from 'mithril';
import { DatalistController } from 'amiv-web-ui-components';
import { joboffers as config } from '../resourceConfig.json';
import TableView from '../views/tableView';
import DatalistController from '../listcontroller';
import { dateFormatter } from '../utils';
import { ResourceHandler } from '../auth';
/* Table of all current Jobs
......@@ -13,7 +14,8 @@ import { dateFormatter } from '../utils';
export default class JobTable {
constructor() {
this.ctrl = new DatalistController('joboffers', {}, config.tableKeys);
this.handler = new ResourceHandler('joboffers', config.tableKeys);
this.ctrl = new DatalistController((query, search) => this.handler.get({ search, ...query }));
}
getItemData(data) {
......
import m from 'mithril';
//import * as mdc from 'material-components-web';
//import "@material/drawer";
import {
List,
ListTile,
......@@ -66,7 +64,7 @@ const layoutStyle = [
width: '100%',
height: '100%',
background: '#000000aa',
'z-index': 100000000
'z-index': 100000000,
},
},
];
......
......@@ -6,7 +6,7 @@ import EditView from '../views/editView';
export default class UserEdit extends EditView {
view() {
return this.layout([
...this.renderPage({
...this.form.renderPage({
lastname: { type: 'text', label: 'Last Name' },
firstname: { type: 'text', label: 'First Name' },
email: { type: 'text', label: 'Email' },
......@@ -19,41 +19,41 @@ export default class UserEdit extends EditView {
{
value: 'none',
label: 'No Member',
defaultChecked: this.data.membership === 'none',
defaultChecked: this.form.data.membership === 'none',
},
{
value: 'regular',
label: 'Regular AMIV Member',
defaultChecked: this.data.membership === 'regular',
defaultChecked: this.form.data.membership === 'regular',
},
{
value: 'extraordinary',
label: 'Extraordinary Member',
defaultChecked: this.data.membership === 'extraordinary',
defaultChecked: this.form.data.membership === 'extraordinary',
},
{
value: 'honorary',
label: 'Honorary Member',
defaultChecked: this.data.membership === 'honorary',
defaultChecked: this.form.data.membership === 'honorary',
},
],
onChange: ({ value }) => { this.data.membership = value; },
onChange: ({ value }) => { this.form.data.membership = value; },
}),
m(RadioGroup, {
name: 'Sex',
buttons: [
{ value: 'female', label: 'Female', defaultChecked: this.data.gender === 'female' },
{ value: 'male', label: 'Male', defaultChecked: this.data.gender === 'male' },
{ value: 'male', label: 'Male', defaultChecked: this.form.data.gender === 'male' },
],
onChange: ({ value }) => { console.log(value); this.data.gender = value; },
onChange: ({ value }) => { console.log(value); this.form.data.gender = value; },
}),
m(RadioGroup, {
name: 'Departement',
buttons: [
{ value: 'itet', label: 'ITET', defaultChecked: this.data.department === 'itet' },
{ value: 'mavt', label: 'MAVT', defaultChecked: this.data.department === 'mavt' },
{ value: 'itet', label: 'ITET', defaultChecked: this.form.data.department === 'itet' },
{ value: 'mavt', label: 'MAVT', defaultChecked: this.form.data.department === 'mavt' },
],
onChange: ({ value }) => { this.data.department = value; },
onChange: ({ value }) => { this.form.data.department = value; },
}),
]);
}
......
import m from 'mithril';
import { DatalistController } from 'amiv-web-ui-components';
import EditUser from './editUser';
import ViewUser from './viewUser';
import TableView from '../views/tableView';
import { users as config } from '../resourceConfig.json';
import DatalistController from '../listcontroller';
import ItemController from '../itemcontroller';
import { loadingScreen } from '../layout';
import { ResourceHandler } from '../auth';
export class UserItem {
constructor() {
......@@ -21,7 +22,11 @@ export class UserItem {
export class UserTable {
constructor() {
this.ctrl = new DatalistController('users', { sort: [['lastname', 1]] });
this.handler = new ResourceHandler('users');
this.ctrl = new DatalistController(
(query, search) => this.handler.get({ search, ...query }),
{ sort: [['lastname', 1]] },
);
}
view() {
return m(TableView, {
......
import m from 'mithril';
import { Card, Toolbar, ToolbarTitle, Button } from 'polythene-mithril';
import { ListSelect, DatalistController } from 'amiv-web-ui-components';
import ItemView from '../views/itemView';
import TableView from '../views/tableView';
import SelectList from '../views/selectList';
import DatalistController from '../listcontroller';
import RelationlistController from '../relationlistcontroller';
import { ResourceHandler } from '../auth';
import { chip, icons, Property } from '../views/elements';
import { colors } from '../style';
......@@ -23,7 +23,9 @@ export default class UserView extends ItemView {
// (this will be displayed once the user clicks on 'new')
this.groupchoice = false;
// a controller to handle the list of possible groups to join
this.groupcontroller = new DatalistController('groups', {}, ['name']);
this.groupHandler = new ResourceHandler('groups', ['name']);
this.groupController = new DatalistController((query, search) =>
this.groupHandler.get({ search, ...query }));
// exclude the groups where the user is already a member
this.groupmemberships.handler.get({ where: { user: this.data._id } })
.then((data) => {
......@@ -68,8 +70,8 @@ export default class UserView extends ItemView {
// Selector that is only displayed if "new" is clicked in the
// groupmemberships. Selects a group to request membership for.
const groupSelect = m(SelectList, {
controller: this.groupcontroller,
const groupSelect = m(ListSelect, {
controller: this.groupController,
listTileAttrs: group => Object.assign({}, { title: group.name }),
selectedText: group => group.name,
onSubmit: (group) => {
......@@ -79,6 +81,7 @@ export default class UserView extends ItemView {
group: group._id,
}).then(() => {
this.groupmemberships.refresh();
m.redraw();
});
},
onCancel: () => { this.groupchoice = false; m.redraw(); },
......
import Ajv from 'ajv';
import { Checkbox, IconButton, Toolbar, ToolbarTitle, Button } from 'polythene-mithril';
import m from 'mithril';
import { IconButton, Toolbar, ToolbarTitle, Button } from 'polythene-mithril';
import { Form } from 'amiv-web-ui-components';
// eslint-disable-next-line import/extensions
import { apiUrl } from 'networkConfig';
import ItemView from './itemView';
import { textInput, datetimeInput, numInput, icons } from './elements';
import { icons } from './elements';
import { colors } from '../style';
const m = require('mithril');
// Mapper for resource vs schema-object names
const objectNameForResource = {
users: 'User',
......@@ -34,147 +33,17 @@ export default class EditView extends ItemView {
*/
constructor(vnode, valid = true) {
super(vnode);
this.changed = false;
// state for validation
this.valid = valid;
this.ajv = new Ajv({
missingRefs: 'ignore',
errorDataPath: 'property',
allErrors: true,
});
this.errors = {};
// copy a local version of the controller data to manipulate before submission
// (changes will therefore not be applied if edit is cancelled)
this.data = Object.assign({}, this.controller.data);
// start a form to collect the submit data
this.form = new Form({}, valid, Object.assign({}, this.controller.data));
}
oninit() {
// load schema
m.request(`${apiUrl}/docs/api-docs`).then((schema) => {
const objectSchema = schema.definitions[
objectNameForResource[this.resource]];
// console.log(objectSchema);
// filter out any field that is not understood by the validator tool
Object.keys(objectSchema.properties).forEach((property) => {
if (objectSchema.properties[property].type === 'media' ||
objectSchema.properties[property].type === 'json_schema_object') {
objectSchema.properties[property].type = 'object';
}
if (objectSchema.properties[property].format === 'objectid') {
delete objectSchema.properties[property];
}
// translate nullable field from OpenAPI specification to
// possible type null in jsonschema
if (objectSchema.properties[property].nullable) {
objectSchema.properties[property].type = [
'null',
objectSchema.properties[property].type,
];
}
});
// delete objectSchema.properties['_id'];
console.log(this.ajv.addSchema(objectSchema, 'schema'));
this.validate();
this.form.setSchema(schema.definitions[objectNameForResource[this.resource]]);
}).catch((error) => { console.log(error); });
}
/**
* bind form-fields to the object data and validation
*
* A binded form-field automatically updates this.data and calls validation
* on the current data state with every change.
*
* @param {object} options for the input element
* @return {object} modified options passed to the input element
*/
bind(attrs) {
// initialize error-list for every bound field
if (!this.errors[attrs.name]) this.errors[attrs.name] = [];
const boundFormelement = {
onChange: (name, value) => {
this.changed = true;
// bind changed data
this.data[name] = value;
console.log(this.data);
this.validate();
},
getErrors: () => this.errors[attrs.name],
value: this.data[attrs.name],
};
// add the given attributes
Object.keys(attrs).forEach((key) => { boundFormelement[key] = attrs[key]; });
return boundFormelement;
}
validate() {
// validate against schema
const validate = this.ajv.getSchema('schema');
// sometimes the schema loading does not work or is not finished
// before the first edit, this is to prevent crashes
if (validate) {
this.valid = validate(this.data);
console.log(validate.errors);
if (this.valid) {
Object.keys(this.errors).forEach((field) => {
this.errors[field] = [];
});
} else {
// get errors for respective fields
Object.keys(this.errors).forEach((field) => {
const errors = validate.errors.filter(error =>
`.${field}` === error.dataPath);
this.errors[field] = errors.map(error => error.message);
});
}
}
m.redraw();
}
/**
* Rendering Function to make form descriptions shorter
*
* @param {object} Collection of descriptions for input form fields
* {key: description}
* with key matching the field in this.data
* description containing type in ['text', 'number',
* 'checkbox', 'datetime'] and any attributes passed to the
* input element
* @return {string} mithril rendered output
*/
renderPage(page) {
return Object.keys(page).map((key) => {
const field = page[key];
if (field.type === 'text') {
field.name = key;
field.floatingLabel = true;
delete field.type;
return m(textInput, this.bind(field));
} else if (field.type === 'number') {
field.name = key;
field.floatingLabel = true;
delete field.type;
return m(numInput, this.bind(field));
} else if (field.type === 'checkbox') {
field.checked = this.data[key] || false;
field.onChange = ({ checked }) => {
this.data[key] = checked;
};
delete field.type;
return m(Checkbox, field);
} else if (field.type === 'datetime') {
field.name = key;
delete field.type;
return m(datetimeInput, this.bind(field));
}
return `key '${key}' not found`;
});
}
/**
* Submit the changed version of this.data
*
......@@ -183,13 +52,13 @@ export default class EditView extends ItemView {
* changes.
*/
submit(formData = false) {
if (Object.keys(this.data).length > 0) {
if (Object.keys(this.form.data).length > 0) {
let request;
if (this.controller.modus === 'edit') {
// if id is known, this is a patch to an existing item
request = this.controller.patch(this.data, formData);
request = this.controller.patch(this.form.data, formData);
} else {
request = this.controller.post(this.data);
request = this.controller.post(this.form.data);
}
request.catch((error) => {
console.log(error);
......@@ -226,7 +95,7 @@ export default class EditView extends ItemView {
m(Button, {
className: 'blue-button-filled',
label: 'submit',
disabled: !this.valid,
disabled: !this.form.valid,
events: { onclick: () => { this.beforeSubmit(); } },
}),
]),
......
import m from 'mithril';
import {
IconButton,
TextField,
Toolbar,
ToolbarTitle,
Card,
Icon,
} from 'polythene-mithril';
import { Icon } from 'polythene-mithril';
export const icons = {
search: '<svg width="24" height="24" viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>',
......@@ -28,231 +21,6 @@ export const icons = {
menu: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/></svg>',
};
export class textInput {
constructor({ attrs: { getErrors, name } }) {
// Link the error-getting function from the binding
this.getErrors = () => [];
this.name = name;
if (getErrors) {
this.getErrors = getErrors;
}
this.value = '';
}
view({ attrs }) {
// set display-settings accoridng to error-state
const errors = this.getErrors();
const attributes = Object.assign({}, attrs);
attributes.valid = errors.length === 0;
attributes.error = errors.join(', ');
attributes.style = Object.assign({
'margin-top': '-10px',
'margin-bottom': '-10px',
}, attributes.style);
attributes.onChange = ({ value }) => {
if (value !== this.value) {
this.value = value;
attrs.onChange(this.name, value);
}
};
return m(TextField, attributes);
}
}
export class numInput extends textInput {
view({ attrs }) {
// set display-settings accoridng to error-state
const errors = this.getErrors();
const attributes = Object.assign({}, attrs);
attributes.type = 'number';
attributes.valid = errors.length === 0;
attributes.error = errors.join(', ');
attributes.style = Object.assign({
'margin-top': '-10px',
'margin-bottom': '-10px',
}, attributes.style);
attributes.onChange = ({ value }) => {
if (value !== this.value) {
this.value = value;
attrs.onChange(this.name, parseInt(value, 10));
}
};
return m(TextField, attributes);
}
}
export class datetimeInput {
constructor({ attrs: { getErrors, name, onChange } }) {
// Link the error-getting function from the binding
this.getErrors = () => [];
this.name = name;
if (getErrors) { this.getErrors = getErrors; }
this.value = '';
this.date = false;
this.time = false;
this.onChangeCallback = onChange;
}
onChange() {
if (this.date && this.time) {
const date = new Date(this.date);
const splitted = this.time.split(':');
date.setHours(splitted[0]);
date.setMinutes(splitted[1]);
if (this.onChangeCallback) {