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 1fd24197 authored by Sandro Lutz's avatar Sandro Lutz Committed by Sandro Lutz
Browse files

Add ActionBar to filtered list pages

parent 74e10e20
import m from 'mithril';
import './ActionBar.less';
export default class ActionBarComponent {
/**
* Generic action bar component
*
* @param {string} attrs All other attributes are assigned to the container.
* @param {string} attrs.left Buttons to place on the left side.
* @param {string} attrs.right Buttons to place on the right side.
*
* Example:
* ```javascript
* m(TooltipComponent, {
* left: [m(Button, { label: 'btn left'})],
* right: [m(Button, { label: 'btn right'})],
* })
* ```
*/
static view({ attrs: { left, right, ...attrs } }) {
return m('.pe-actionbar', attrs, m('div', left), m('div', right));
}
}
.pe-actionbar {
width: 100%;
display: flex;
justify-content: space-between;
> div > * {
padding: 6px;
margin: 0;
}
}
......@@ -3,7 +3,7 @@ import { Button } from 'polythene-mithril-button';
import { ButtonCSS } from 'polythene-css';
ButtonCSS.addStyle('.blue-button', {
color_light_background: '#5378E1',
color_light_background: '#3f51b5',
color_light_text: 'white',
color_dark_background: '#1f2d54',
color_dark_text: 'white',
......@@ -16,6 +16,17 @@ ButtonCSS.addStyle('.red-button', {
color_dark_text: 'white',
});
ButtonCSS.addStyle('.blue-flat-button', {
color_light_background: 'transparent',
color_light_border: '#3f51b5',
color_light_text: '#3f51b5',
});
ButtonCSS.addStyle('.red-flat-button', {
color_light_background: 'transparent',
color_light_text: '#e8462b',
});
ButtonCSS.addStyle('.flat-button', {
color_light_background: 'transparent',
color_light_text: 'black',
......
......@@ -9,3 +9,4 @@ export { default as SelectGroupForm } from './form/selectGroup';
export { default as FileInput } from './FileInput';
export { default as EventCard } from './EventCard';
export { default as Select } from './Select';
export { default as ActionBar } from './ActionBar';
......@@ -7,9 +7,11 @@ export default {
// Icons provided by our web-ui-components library
...icons,
link:
'<svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="#000000" d="M3.9,12C3.9,10.29 5.29,8.9 7,8.9H11V7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H11V15.1H7C5.29,15.1 3.9,13.71 3.9,12M8,13H16V11H8V13M17,7H13V8.9H17C18.71,8.9 20.1,10.29 20.1,12C20.1,13.71 18.71,15.1 17,15.1H13V17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7Z" /></svg>',
filterList:
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/><path d="M0 0h24v24H0z" fill="none"/></svg>',
link:
externalLink:
'<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="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>',
earth:
'<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path fill="#000" d="M17.9,17.39C17.64,16.59 16.89,16 16,16H15V13A1,1 0 0,0 14,12H8V10H10A1,1 0 0,0 11,9V7H13A2,2 0 0,0 15,5V4.59C17.93,5.77 20,8.64 20,12C20,14.08 19.2,15.97 17.9,17.39M11,19.93C7.05,19.44 4,16.08 4,12C4,11.38 4.08,10.78 4.21,10.21L9,15V16A2,2 0 0,0 11,18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></svg>',
......
......@@ -25,6 +25,7 @@ export default {
search: 'Suchen',
reset: 'Zurücksetzen',
externalLink: 'Externer Link',
copyDirectLink: 'Direktlink kopieren',
button: {
clear: 'löschen',
create: 'erstellen',
......@@ -281,7 +282,7 @@ export default {
// Joboffers Page
joboffers: {
title: 'Jobs',
downloadAsPdf: 'Als PDF herunterladen',
downloadAsPdf: 'PDF herunterladen',
publishedToday: 'heute veröffentlicht',
publishedYesterday: 'gestern veröffentlicht',
publishedDaysAgo: 'vor {{days}} Tagen veröffentlicht',
......
......@@ -25,6 +25,7 @@ export default {
search: 'Search',
reset: 'Reset',
externalLink: 'External link',
copyDirectLink: 'Copy link',
button: {
clear: 'clear',
create: 'create',
......@@ -281,7 +282,7 @@ export default {
// Joboffers Page
joboffers: {
title: 'Jobs',
downloadAsPdf: 'Download as PDF',
downloadAsPdf: 'Download PDF',
publishedToday: 'published today',
publishedYesterday: 'published yesterday',
publishedDaysAgo: 'published {{days}} days ago',
......
......@@ -2,8 +2,10 @@
.event-details {
display: grid;
grid-template-areas: 'description separator signup';
grid-template-columns: 1fr 4px 350px;
grid-template-areas:
'signup separator description'
'actions actions actions';
grid-template-columns: 350px 4px 1fr;
border-top: 1px solid @color-grey;
align-items: center;
min-height: 10em;
......@@ -13,10 +15,26 @@
grid-template-areas:
'description'
'separator'
'signup';
'signup'
'actions';
min-height: unset;
}
&.no-signup {
grid-template-areas:
'description'
'actions';
grid-template-columns: 1fr;
.separator {
display: none;
}
.form {
display: none;
}
}
> p {
grid-area: description;
margin: 1em;
......@@ -39,8 +57,7 @@
.form {
grid-area: signup;
position: relative;
margin: 2em 1em;
text-align: center;
margin: 2em 1em .5em;
> p {
position: absolute;
......@@ -65,6 +82,10 @@
}
}
.event-actions {
grid-area: actions;
}
.event-loading {
position: relative;
width: 100%;
......
.joboffer-details {
padding: 0 2em 2em;
p { margin: 1em 0; }
a { padding: 0; }
.description {
padding: 0 2em;
}
}
.joboffer-header {
display: grid;
grid-template-areas: 'logo content';
grid-template-columns: 150px 1fr;
grid-template-columns: 138px 1fr;
@media @mobile {
grid-template-areas: 'content';
......@@ -9,8 +9,10 @@
}
.image {
width: unset;
grid-area: logo;
display: block;
margin: 16px;
background-color: transparent;
@media @mobile {
......
......@@ -37,7 +37,7 @@
}
.studydoc-header {
padding: 1em;
padding: 1.25em;
.title {
display: block;
......@@ -45,7 +45,6 @@
font-size: 1em;
h3 {
font-size: 1.2em;
margin: 0;
display: inline-block;
......@@ -63,12 +62,12 @@
.properties {
width: 100%;
margin-top: 1em;
margin-top: .25em;
}
.property {
display: inline-block;
padding-bottom: 1em;
padding-bottom: .25em;
@media @mobile {
padding-bottom: .25em;
......@@ -107,72 +106,8 @@
.studydoc-content .studydoc-documents {
display: none;
padding: 16px;
background-color: @color-grey;
@media @mobile, @tablet {
display: block;
}
}
#studydoc-list {
#head-style {
text-align: right;
}
#row-style {
height: 2em;
background-color: #fdfdfd;
border: 1px solid @color-grey;
&:hover {
background-color: #f5f5f5;
}
}
h2 {
display: inline;
text-align: center;
}
.flex-container {
display: flex;
flex-wrap: nowrap;
}
.flex-container > div {
padding: 0 5px 5px 0;
text-align: left;
}
div.list-item {
display: grid;
grid-template-columns: auto 25% 15%;
grid-row-gap: .5em;
padding: .5em;
&:first-of-type {
font-weight: bold;
border-bottom: solid 2px #000;
}
#title-style {
margin-top: auto;
margin-bottom: auto;
}
#author-style {
margin-top: auto;
margin-bottom: auto;
}
#course_year-style {
margin-top: auto;
margin-bottom: auto;
text-align: right;
}
}
div.list-style {
background-color: #808080;
}
}
function copyToClipboard(text, inputElement) {
const input = inputElement;
input.value = text;
input.focus();
input.select();
try {
document.execCommand('copy');
} catch (err) {
console.error('Unable to copy', err);
}
}
export { copyToClipboard };
......@@ -9,8 +9,10 @@ import { Infobox } from '../errors';
import { log } from '../../models/log';
import { isLoggedIn, login } from '../../models/auth';
import Button from '../../components/Button';
import { i18n, currentLocale } from '../../models/language';
import ActionBar from '../../components/ActionBar';
import { i18n, currentLocale, currentLanguage } from '../../models/language';
import icons from '../../images/icons';
import { copyToClipboard } from '../../utils';
export default class EventDetails {
oninit(vnode) {
......@@ -101,13 +103,20 @@ export default class EventDetails {
}
view() {
let noSignup = false;
let eventSignupForm;
let eventSignupButtons;
const now = new Date();
const registerStart = new Date(this.event.time_register_start);
const registerEnd = new Date(this.event.time_register_end);
if (this.event.time_register_start === null) {
eventSignupForm = m('div', m('p', i18n('events.registration.none')));
eventSignupButtons = m(Button, {
className: 'flat-button',
disabled: true,
label: i18n('events.registration.none'),
});
noSignup = true;
} else if (registerStart <= now) {
if (registerEnd >= now) {
if (isLoggedIn()) {
......@@ -131,19 +140,24 @@ export default class EventDetails {
const signupFormOptions = {
signoffButton: this.event.signupData != null,
hasSignupData: this.event.signupData != null,
canChangeSignup: this.schema != null,
};
eventSignupForm = this._renderSignupForm(signupFormOptions);
eventSignupButtons = this._renderSignupButtons(signupFormOptions);
}
} else if (this.event.allow_email_signup) {
const signupFormOptions = {
emailField: true,
};
eventSignupForm = this._renderSignupForm(signupFormOptions);
eventSignupButtons = this._renderSignupButtons(signupFormOptions);
} else {
eventSignupForm = m('div', [
m('span', `${i18n('events.restrictions.membersOnly')} `),
m(Button, { label: i18n('login'), events: { onclick: () => login(m.route.get()) } }),
]);
eventSignupForm = m('div', [m('span', `${i18n('events.restrictions.membersOnly')} `)]);
eventSignupButtons = m(Button, {
label: i18n('login'),
className: 'blue-flat-button',
events: { onclick: () => login(m.route.get()) },
});
}
this._renderParticipationNotice();
} else {
......@@ -184,38 +198,66 @@ export default class EventDetails {
});
}
return m('div.event-details', [
const urlId = `event-${this.event._id}-url`;
return m('div.event-details', { className: noSignup ? 'no-signup' : null }, [
m('p', m.trust(marked(escape(this.event.getDescription())))),
m('div.separator'),
m('div.form', [notification, eventSignupForm]),
!noSignup && m('div.separator'),
!noSignup && m('div.form', [notification, eventSignupForm]),
m(ActionBar, {
className: 'event-actions',
left: eventSignupButtons,
right: [
m('textarea', {
id: urlId,
style: { opacity: 0, width: 0, height: 0, padding: 0 },
}),
m(Button, {
className: 'flat-button',
label: i18n('copyDirectLink'),
events: {
onclick: () => {
const url = `${window.location.origin}/${currentLanguage()}/events/${
this.event._id
}`;
const inputElement = document.getElementById(urlId);
copyToClipboard(url, inputElement);
},
},
}),
],
}),
]);
}
_renderSignupForm({ hasSignupData = false, emailField = false, signoffButton = false }) {
_renderSignupForm({ emailField = false }) {
const elements = this.schema ? this.form.renderSchema() : [];
if (emailField) {
elements.push(this._renderEmailField());
}
if (!hasSignupData) {
elements.push(this._renderSignupButton(i18n('events.signup.action')));
} else if (this.schema) {
elements.push(this._renderSignupButton(i18n('events.signup.updateAction')));
}
if (signoffButton) {
elements.push(this._renderSignoffButton());
}
return m('form', { onsubmit: () => false }, elements);
}
_renderSignupButtons({ canChangeSignup = false, hasSignupData = false, signoffButton = false }) {
return [
(!hasSignupData || canChangeSignup) &&
this._renderSignupButton(
i18n(`events.signup.${hasSignupData ? 'updateAction' : 'action'}`)
),
signoffButton && this._renderSignoffButton(),
];
}
_renderSignupButton(label) {
// TODO: evaluate email field validity!
// Waiting for MR to be accepted in web-ui-components repository.
return m(Button, {
name: 'signup',
className: 'blue-flat-button',
border: true,
label,
active: this.form.valid && !this.signupBusy,
events: {
......@@ -241,6 +283,7 @@ export default class EventDetails {
_renderSignoffButton() {
return m(Button, {
name: 'signoff',
className: 'blue-flat-button',
label: i18n('events.signoff.action'),
active: !this.signoffBusy,
events: {
......
......@@ -48,7 +48,7 @@ export default class Footer {
i18n('footer.issueReport'),
m(Icon, {
class: 'external-link',
svg: { content: m.trust(icons.link) },
svg: { content: m.trust(icons.externalLink) },
size: 'small',
alt: i18n('externalLink'),
}),
......
......@@ -148,7 +148,7 @@ export default class Header {
subitem.url
? m(Icon, {
class: 'external-link',
svg: { content: m.trust(icons.link) },
svg: { content: m.trust(icons.externalLink) },
size: 'small',
alt: i18n('externalLink'),
})
......
......@@ -2,24 +2,50 @@ import m from 'mithril';
import marked from 'marked';
import escape from 'html-escape';
import { apiUrl } from 'config';
import { Button } from 'polythene-mithril-button';
import { i18n } from '../../models/language';
import Button from '../../components/Button';
import ActionBar from '../../components/ActionBar';
import { i18n, currentLanguage } from '../../models/language';
import { copyToClipboard } from '../../utils';
export default class JobofferDetails {
static view({ attrs: { joboffer } }) {
const urlId = `joboffer-${joboffer._id}-url`;
return m('div.joboffer-details', [
m('div.description', m.trust(marked(escape(joboffer.getDescription())))),
joboffer.pdf
? m(Button, {
label: i18n('joboffers.downloadAsPdf'),
border: true,
m(ActionBar, {
right: [
joboffer.pdf
? m(Button, {
className: 'flat-button',
label: i18n('joboffers.downloadAsPdf'),
events: {
onclick: () => {
window.open(apiUrl + joboffer.pdf.file, '_blank');
},
},
})
: null,
m('textarea', {
id: urlId,
style: { opacity: 0, width: 0, height: 0, padding: 0 },
}),
m(Button, {
className: 'flat-button',
label: i18n('copyDirectLink'),
events: {
onclick: () => {
window.open(apiUrl + joboffer.pdf.file, '_blank');
const url = `${window.location.origin}/${currentLanguage()}/${this.name}/${
joboffer._id
}`;
const inputElement = document.getElementById(urlId);
copyToClipboard(url, inputElement);
},
},
})
: null,
}),
],
}),
]);
}
}
......@@ -127,9 +127,9 @@ export default class JobofferList extends FilteredListPage {
},
header: () =>
m('div.joboffer-header', [
m('div.image.ratio-3to2', m('img', { src: imageurl, alt: joboffer.company })),
m('div.image.ratio-4to1', m('img', { src: imageurl, alt: joboffer.company })),
m('div.joboffer-content', [
m('h2.title', joboffer.getTitle()),
m('h3.title', joboffer.getTitle()),
m('div.date', datePhrase),
]),
]),
......
......@@ -4,8 +4,9 @@ import { apiUrl } from 'config';
import filesize from 'filesize';
import ExpansionPanel from 'amiv-web-ui-components/src/expansionPanel';
import { Dialog } from 'polythene-mithril-dialog';
import { Button } from 'polythene-mithril-button';
import { Icon } from 'polythene-mithril-icon';
import Button from '../../components/Button';
import ActionBar from '../../components/ActionBar';
import StudydocsController from '../../models/studydocs';
import { i18n, currentLanguage } from '../../models/language';
import { FilteredListDataStore, FilteredListPage } from '../filteredListPage';
......@@ -13,6 +14,7 @@ import mimeTypeToIcon from '../../images/mimeTypeToIcon';
import StudydocQuickFilter from './studydocQuickFilter';
import { Infobox } from '../errors';
import icons from '../../images/icons';
import { copyToClipboard } from '../../utils';
const controller = new StudydocsController();
const dataStore = new FilteredListDataStore();
......@@ -22,6 +24,8 @@ export default class StudydocList extends FilteredListPage {
super('studydocuments', dataStore);
this.dropdownDisabled = {};
this.directLinkCopied = null;
this.directLinkCopiedTimeout = null;
}
oninit(vnode) {
......@@ -288,24 +292,29 @@ export default class StudydocList extends FilteredListPage {
? studydocument.title
: i18n('studydocs.name.default');
const urlId = `studydoc-${studydocument._id}-url`;
return m(ExpansionPanel, {
id: this.getItemElementId(studydocument._id),
expanded: studydocument._id === selectedId,
separated: true,
duration: animationDuration,
onChange: expanded => {
if (this.expandDisabled === studydocument._id) {
this.expandDisabled = null;
return;
}
this.onChange(studydocument._id, expanded, animationDuration);
},