Merge pull request #4638 from ietf-tools/main

chore: merge main into feat/tzaware
This commit is contained in:
Jennifer Richards 2022-10-24 12:57:11 -03:00 committed by GitHub
commit d55280d0b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
105 changed files with 1364 additions and 829 deletions

View file

@ -34,7 +34,7 @@ indent_size = 2
[dev/**.js]
indent_size = 2
[{package.json,.eslintrc.js,.yarnrc.yml,vite.config.js,cypress.config.js}]
[{package.json,.eslintrc.js,.yarnrc.yml,vite.config.js,jsconfig.json}]
indent_size = 2
# Settings for cypress tests

847
.pnp.cjs generated

File diff suppressed because it is too large Load diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -13,7 +13,22 @@
.agenda-topnav.my-3
meeting-navigation
n-button.d-none.d-sm-flex(
.agenda-topnav-right.d-none.d-md-flex
n-button(
quaternary
@click='startTour'
)
template(#icon)
i.bi.bi-question-square
span Help
n-button(
quaternary
@click='toggleShare'
)
template(#icon)
i.bi.bi-share
span Share
n-button(
quaternary
@click='toggleSettings'
)
@ -137,6 +152,7 @@
agenda-quick-access
agenda-mobile-bar
agenda-share-modal(v-model:shown='state.shareModalShown')
</template>
<script setup>
@ -159,10 +175,12 @@ import AgendaScheduleList from './AgendaScheduleList.vue'
import AgendaScheduleCalendar from './AgendaScheduleCalendar.vue'
import AgendaQuickAccess from './AgendaQuickAccess.vue'
import AgendaSettings from './AgendaSettings.vue'
import AgendaShareModal from './AgendaShareModal.vue'
import AgendaMobileBar from './AgendaMobileBar.vue'
import MeetingNavigation from './MeetingNavigation.vue'
import timezones from '../shared/timezones'
import { initTour } from './tour'
import { useAgendaStore } from './store'
import { useSiteStore } from '../shared/store'
@ -187,6 +205,7 @@ const route = useRoute()
const state = reactive({
searchText: '',
shareModalShown: false
})
// REFS
@ -219,8 +238,19 @@ watch(() => agendaStore.meetingDays, () => {
})
watch(() => agendaStore.isLoaded, () => {
let resetQuery = false
if (route.query.filters) {
// Handle ?filters= parameter
const keywords = route.query.filters.split(',').map(k => k.trim()).filter(k => !!k)
if (keywords?.length > 0) {
agendaStore.$patch({
selectedCatSubs: keywords
})
}
resetQuery = true
}
if (route.query.show) {
// Handle legacy ?show= parameter
// Handle ?show= parameter
const keywords = route.query.show.split(',').map(k => k.trim()).filter(k => !!k)
if (keywords?.length > 0) {
const pickedIds = []
@ -235,13 +265,23 @@ watch(() => agendaStore.isLoaded, () => {
pickerModeView: true,
pickedEvents: pickedIds
})
agendaStore.persistMeetingPreferences()
}
}
resetQuery = true
}
if (route.query.pick) {
// Handle legacy /personalize path (open picker mode)
agendaStore.$patch({ pickerMode: true })
resetQuery = true
}
if (route.query.tz) {
// Handle tz param
agendaStore.$patch({ timezone: route.query.tz })
resetQuery = true
}
if (resetQuery) {
agendaStore.persistMeetingPreferences()
router.replace({ query: null })
}
@ -313,6 +353,18 @@ function toggleSettings () {
})
}
function toggleShare () {
state.shareModalShown = !state.shareModalShown
}
function startTour () {
const tour = initTour({
mobileMode: siteStore.viewport < 990,
pickerMode: agendaStore.pickerMode
})
tour.start()
}
// -> Go to current meeting if not provided
function handleCurrentMeetingRedirect () {
if (!route.params.meetingNumber && agendaStore.meeting.number) {
@ -394,15 +446,6 @@ onMounted(() => {
}
})
// CREATED
// -> Handle loading tab directly based on URL
if (window.location.pathname.indexOf('-utc') >= 0) {
agendaStore.$patch({ timezone: 'UTC' })
} else if (window.location.pathname.indexOf('personalize') >= 0) {
// state.currentTab = 'personalize'
}
</script>
<style lang="scss">
@ -421,11 +464,17 @@ if (window.location.pathname.indexOf('-utc') >= 0) {
&-topnav {
position: relative;
> button {
&-right {
position: absolute;
top: 5px;
right: 0;
display: flex;
button + button {
margin-left: 5px;
}
> button:last-child {
.bi {
transition: transform 1s ease;
}
@ -437,6 +486,7 @@ if (window.location.pathname.indexOf('-utc') >= 0) {
}
}
}
}
&-tz-selector {
margin-right: .5rem;

View file

@ -64,7 +64,7 @@ n-drawer(v-model:show='state.isShown', placement='bottom', :height='state.drawer
</template>
<script setup>
import { reactive, ref, unref, watch } from 'vue'
import { nextTick, reactive, ref, unref, watch } from 'vue'
import intersection from 'lodash/intersection'
import difference from 'lodash/difference'
import union from 'lodash/union'
@ -113,8 +113,15 @@ function cancelFilter () {
}
function saveFilter () {
const applyLoadingMsg = message.create('Applying filters...', { type: 'loading', duration: 0 })
setTimeout(() => {
agendaStore.$patch({ selectedCatSubs: state.pendingSelection })
agendaStore.persistMeetingPreferences()
state.isShown = false
nextTick(() => {
applyLoadingMsg.destroy()
})
}, 500)
}
function clearFilter () {

View file

@ -0,0 +1,173 @@
<template lang="pug">
n-modal(v-model:show='modalShown')
n-card.agenda-share(
:bordered='false'
segmented
role='dialog'
aria-modal='true'
)
template(#header-extra)
.agenda-share-header
n-button.ms-4.agenda-share-close(
ghost
color='gray'
strong
@click='modalShown = false'
)
i.bi.bi-x
template(#header)
.agenda-share-header
i.bi.bi-share
span Share this view
.agenda-share-content
.text-muted.pb-2 Use the following URL for sharing the current view #[em (including any active filters)] with other users:
n-input-group
n-input(
ref='filteredUrlIpt'
size='large'
readonly
v-model:value='state.filteredUrl'
)
n-button(
type='primary'
primary
strong
size='large'
@click='copyFilteredUrl'
)
template(#icon)
i.bi.bi-clipboard-check.me-1
span Copy
</template>
<script setup>
import { computed, reactive, ref, watch } from 'vue'
import { find } from 'lodash-es'
import {
NButton,
NCard,
NModal,
NInputGroup,
NInput,
useMessage
} from 'naive-ui'
import { useAgendaStore } from './store'
// PROPS
const props = defineProps({
shown: {
type: Boolean,
required: true,
default: false
}
})
// MESSAGE PROVIDER
const message = useMessage()
// STORES
const agendaStore = useAgendaStore()
// EMIT
const emit = defineEmits(['update:shown'])
// STATE
const state = reactive({
isLoading: false,
filteredUrl: window.location.href
})
const filteredUrlIpt = ref(null)
// COMPUTED
const modalShown = computed({
get () {
return props.shown
},
set(value) {
emit('update:shown', value)
}
})
// WATCHERS
watch(() => props.shown, (newValue) => {
if (newValue) {
generateUrl()
}
})
// METHODS
function generateUrl () {
const newUrl = new URL(window.location.href)
const queryParams = []
if (agendaStore.selectedCatSubs.length > 0 ) {
queryParams.push(`filters=${agendaStore.selectedCatSubs.join(',')}`)
}
if (agendaStore.pickerMode && agendaStore.pickedEvents.length > 0 ) {
const kwds = []
for (const id of agendaStore.pickedEvents) {
const session = find(agendaStore.scheduleAdjusted, ['id', id])
if (session) {
const suffix = session.sessionToken ? `-${session.sessionToken}` : ''
kwds.push(`${session.acronym}${suffix}`)
}
}
queryParams.push(`show=${kwds.join(',')}`)
}
newUrl.search = queryParams.length > 0 ? `?${queryParams.join('&')}` : ''
state.filteredUrl = newUrl.toString()
}
async function copyFilteredUrl () {
filteredUrlIpt.value?.select()
try {
if (navigator.clipboard) {
await navigator.clipboard.writeText(state.filteredUrl)
} else {
if (!document.execCommand('copy')) {
throw new Error('Copy failed')
}
}
message.success('URL copied to clipboard successfully.')
} catch (err) {
message.error('Failed to copy URL to clipboard.')
}
}
</script>
<style lang="scss">
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
.agenda-share {
width: 90vw;
max-width: 1000px;
&-header {
font-size: 20px;
display: flex;
align-items: center;
> .bi {
margin-right: 12px;
font-size: 20px;
color: $indigo;
}
}
&-close .bi {
font-size: 20px;
color: inherit;
}
}
</style>

View file

@ -4,6 +4,7 @@ import uniqBy from 'lodash/uniqBy'
import murmur from 'murmurhash-js/murmurhash3_gc'
import { useSiteStore } from '../shared/store'
import { storageAvailable } from '../shared/feature-detect'
const urlRe = /http[s]?:\/\/(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+/
const conferenceDomains = ['webex.com', 'zoom.us', 'jitsi.org', 'meetecho.com', 'gather.town']
@ -147,7 +148,11 @@ export const useAgendaStore = defineStore('agenda', {
const agendaData = await resp.json()
// -> Switch to meeting timezone
if (storageAvailable('localStorage')) {
this.timezone = window.localStorage.getItem(`agenda.${agendaData.meeting.number}.timezone`) || agendaData.meeting.timezone
} else {
this.timezone = agendaData.meeting.timezone
}
// -> Load meeting data
this.categories = agendaData.categories
@ -161,9 +166,17 @@ export const useAgendaStore = defineStore('agenda', {
this.infoNoteHash = murmur(agendaData.meeting.infoNote, 0).toString()
// -> Load meeting-specific preferences
if (storageAvailable('localStorage')) {
this.infoNoteShown = !(window.localStorage.getItem(`agenda.${agendaData.meeting.number}.hideInfo`) === this.infoNoteHash)
this.colorAssignments = JSON.parse(window.localStorage.getItem(`agenda.${agendaData.meeting.number}.colorAssignments`) || '{}')
this.selectedCatSubs = JSON.parse(window.localStorage.getItem(`agenda.${agendaData.meeting.number}.filters`) || '[]')
this.pickedEvents = JSON.parse(window.localStorage.getItem(`agenda.${agendaData.meeting.number}.pickedEvents`) || '[]')
} else {
this.infoNoteShown = true
this.colorAssignments = {}
this.selectedCatSubs = []
this.pickedEvents = []
}
this.isLoaded = true
} catch (err) {
@ -177,12 +190,15 @@ export const useAgendaStore = defineStore('agenda', {
this.hideLoadingScreen()
},
persistMeetingPreferences () {
if (!storageAvailable('localStorage')) { return }
if (this.infoNoteShown) {
window.localStorage.removeItem(`agenda.${this.meeting.number}.hideInfo`)
} else {
window.localStorage.setItem(`agenda.${this.meeting.number}.hideInfo`, this.infoNoteHash)
}
window.localStorage.setItem(`agenda.${this.meeting.number}.colorAssignments`, JSON.stringify(this.colorAssignments))
window.localStorage.setItem(`agenda.${this.meeting.number}.filters`, JSON.stringify(this.selectedCatSubs))
window.localStorage.setItem(`agenda.${this.meeting.number}.pickedEvents`, JSON.stringify(this.pickedEvents))
window.localStorage.setItem(`agenda.${this.meeting.number}.timezone`, this.timezone)
},
@ -221,10 +237,10 @@ export const useAgendaStore = defineStore('agenda', {
}
},
persist: {
enabled: true,
enabled: storageAvailable('localStorage'),
strategies: [
{
storage: localStorage,
storage: storageAvailable('localStorage') ? localStorage : null,
paths: [
'areaIndicatorsShown',
'bolderText',

113
client/agenda/tour.js Normal file
View file

@ -0,0 +1,113 @@
import Shepherd from 'shepherd.js'
import 'shepherd.js/dist/css/shepherd.css'
export function initTour ({ mobileMode, pickerMode }) {
const tour = new Shepherd.Tour({
useModalOverlay: true,
defaultStepOptions: {
classes: 'shepherd-theme-custom',
scrollTo: false,
modalOverlayOpeningPadding: 8,
modalOverlayOpeningRadius: 4,
popperOptions: {
modifiers: [
{
name: 'offset',
options: {
offset: [0,20]
}
}
]
}
}
})
const defaultButtons = [
{
text: 'Exit',
action: tour.cancel,
secondary: true
},
{
text: 'Next',
action: tour.next
}
]
// STEPS
tour.addSteps([
{
title: 'Filter Areas + Groups',
text: 'You can filter the list of sessions by areas or working groups you\'re interested in. The filters you select here also apply to the <strong>Calendar View</strong> and persist even if you come back to this page later.',
attachTo: {
element: mobileMode ? '.agenda-mobile-bar > button:first-child' : '#agenda-quickaccess-filterbyareagroups-btn',
on: mobileMode ? 'top' : 'left'
},
buttons: defaultButtons
},
{
title: 'Pick Sessions',
text: 'Alternatively select <strong>individual sessions</strong> from the list to build your own schedule.',
attachTo: {
element: pickerMode ? '.agenda-quickaccess-btnrow' : '#agenda-quickaccess-picksessions-btn',
on: 'left'
},
buttons: defaultButtons,
showOn: () => !mobileMode
},
{
title: 'Calendar View',
text: 'View the current list of sessions in a <strong>calendar view</strong>, by week or by day. The filters you selected above also apply in this view.',
attachTo: {
element: mobileMode ? '.agenda-mobile-bar > button:nth-child(2)' : '#agenda-quickaccess-calview-btn',
on: mobileMode ? 'top' : 'left'
},
buttons: defaultButtons
},
{
title: 'Add to your calendar',
text: 'Add the current list of sessions to your personal calendar application, in either <strong>webcal</strong> or <strong>ics</strong> format.',
attachTo: {
element: mobileMode ? '.agenda-mobile-bar > button:nth-child(3)' : '#agenda-quickaccess-addtocal-btn',
on: mobileMode ? 'top' : 'left'
},
buttons: defaultButtons
},
{
title: 'Search Events',
text: 'Filter the list of sessions by searching for <strong>specific keywords</strong> in the title, location, acronym, notes or group name. Click the button again to close the search and discard its filtering.',
attachTo: {
element: '.agenda-table-search',
on: 'top'
},
buttons: defaultButtons
},
{
title: 'Assign Colors to Events',
text: 'Assign colors to individual events to keep track of those you find interesting, wish to attend or define your own colors / descriptions from the <strong>Settings</strong> panel.',
attachTo: {
element: '.agenda-table-colorpicker',
on: 'top'
},
buttons: defaultButtons
},
{
title: 'Sessions',
text: 'View the session materials by either clicking on its title or using the <strong>Show meeting materials</strong> button on the right. You can locate the room holding this event on the floor plan by clicking on the location name.',
attachTo: {
element: () => document.querySelector('.agenda-table-display-event'),
on: 'top'
},
buttons: [
{
text: 'Finish',
action: tour.next
}
],
modalOverlayOpeningPadding: 0,
modalOverlayOpeningRadius: 2
}
])
return tour
}

View file

@ -29,6 +29,12 @@ export default createRouter({
return { name: 'agenda' }
}
},
{
path: '/meeting/:meetingNumber(\\d+)?/agenda-utc',
redirect: to => {
return { name: 'agenda', query: { ...to.query, tz: 'UTC' } }
}
},
{
path: '/meeting/:meetingNumber(\\d+)?/agenda/personalize',
redirect: to => {

View file

@ -0,0 +1,19 @@
const cache = {}
export function storageAvailable(type) {
if (Object.prototype.hasOwnProperty.call(cache, type)) {
return cache[type]
}
try {
let storage = window[type]
const x = '__storage_test__'
storage.setItem(x, x)
storage.removeItem(x)
cache[type] = true
return true
}
catch (e) {
cache[type] = false
return false
}
}

View file

@ -595,7 +595,7 @@ def to_iesg(request,name):
e.by = by
e.doc = doc
e.rev = doc.rev
e.desc = "IESG process started in state <b>%s</b>" % target_state['iesg'].name
e.desc = "Document is now in IESG state <b>%s</b>" % target_state['iesg'].name
e.save()
events.append(e)
@ -716,7 +716,7 @@ def edit_info(request, name):
e.by = by
e.doc = doc
e.rev = doc.rev
e.desc = "IESG process started in state <b>%s</b>" % doc.get_state("draft-iesg").name
e.desc = "Document is now in IESG state <b>%s</b>" % doc.get_state("draft-iesg").name
e.save()
events.append(e)

View file

@ -116,6 +116,9 @@ def get_person_form(*args, **kwargs):
self.initial["ascii"] = ""
self.fields['pronouns_selectable'] = forms.MultipleChoiceField(label='Pronouns', choices = [(option, option) for option in ["he/him", "she/her", "they/them"]], widget=forms.CheckboxSelectMultiple, required=False)
self.fields["pronouns_freetext"].widget.attrs.update(
{"aria-label": "Optionally provide your personal pronouns"}
)
self.unidecoded_ascii = False

View file

@ -96,6 +96,7 @@ def preprocess_assignments_for_agenda(assignments_queryset, meeting, extra_prefe
a.session.historic_parent
a.session.rescheduled_to (if rescheduled)
a.session.prefetched_active_materials
a.session.order_number
"""
assignments_queryset = assignments_queryset.prefetch_related(
'timeslot', 'timeslot__type', 'timeslot__meeting',

View file

@ -1095,8 +1095,18 @@ class Session(models.Model):
from ietf.meeting.utils import add_event_info_to_session_qs
if self.group.features.has_meetings:
if not hasattr(self, "_all_meeting_sessions_for_group_cache"):
sessions = [s for s in add_event_info_to_session_qs(self.meeting.session_set.filter(group=self.group,type=self.type)) if s.official_timeslotassignment()]
self._all_meeting_sessions_for_group_cache = sorted(sessions, key = lambda x: x.official_timeslotassignment().timeslot.time)
sessions = [s for s in add_event_info_to_session_qs(self.meeting.session_set.filter(group=self.group)) if s.official_timeslotassignment()]
for s in sessions:
s.ota = s.official_timeslotassignment()
# Align this sort with SchedTimeSessAssignment default sort order since many views base their order on that
self._all_meeting_sessions_for_group_cache = sorted(
sessions, key = lambda x: (
x.ota.timeslot.time,
x.ota.timeslot.type.slug,
x.ota.session.group.parent.name if x.ota.session.group.parent else None,
x.ota.session.name
)
)
return self._all_meeting_sessions_for_group_cache
else:
return [self]

View file

@ -210,8 +210,7 @@ def materials_document(request, document, num=None, ext=None):
if (re.search(r'^\w+-\d+-.+-\d\d$', document) or
re.search(r'^\w+-interim-\d+-.+-\d\d-\d\d$', document) or
re.search(r'^\w+-interim-\d+-.+-sess[a-z]-\d\d$', document) or
re.search(r'^minutes-interim-\d+-.+-\d\d$', document) or
re.search(r'^slides-interim-\d+-.+-\d\d$', document)):
re.search(r'^(minutes|slides|chatlog|polls)-interim-\d+-.+-\d\d$', document)):
name, rev = document.rsplit('-', 1)
else:
name, rev = document, None
@ -1640,6 +1639,8 @@ def api_get_agenda_data (request, num=None):
# Get Floor Plans
floors = FloorPlan.objects.filter(meeting=meeting).order_by('order')
#debug.show('all([(item.acronym,item.session.order_number,item.session.order_in_meeting()) for item in filtered_assignments])')
return JsonResponse({
"meeting": {
"number": schedule.meeting.number,
@ -1731,7 +1732,7 @@ def agenda_extract_schedule (item):
} if item.session.agenda() is not None else {
"url": None
},
"orderInMeeting": item.session.order_in_meeting(),
"orderInMeeting": item.session.order_number,
"short": item.session.short if item.session.short else item.session.short_name,
"sessionToken": item.session.docname_token_only_for_multiple(),
"links": {

View file

@ -267,6 +267,16 @@ def status_slug_for_new_session(session, session_number):
return 'schedw'
def get_outbound_conflicts(form: SessionForm):
"""extract wg conflict constraint data from a SessionForm"""
outbound_conflicts = []
for conflictname, cfield_id in form.wg_constraint_field_ids():
conflict_groups = form.cleaned_data[cfield_id]
if len(conflict_groups) > 0:
outbound_conflicts.append(dict(name=conflictname, groups=conflict_groups))
return outbound_conflicts
@role_required(*AUTHORIZED_ROLES)
def confirm(request, acronym):
'''
@ -299,12 +309,8 @@ def confirm(request, acronym):
session_data['timeranges_display'] = [t.desc for t in form.cleaned_data['timeranges']]
session_data['resources'] = [ ResourceAssociation.objects.get(pk=pk) for pk in request.POST.getlist('resources') ]
# extract wg conflict constraint data for the view
outbound_conflicts = []
for conflictname, cfield_id in form.wg_constraint_field_ids():
conflict_groups = form.cleaned_data[cfield_id]
if len(conflict_groups) > 0:
outbound_conflicts.append(dict(name=conflictname, groups=conflict_groups))
# extract wg conflict constraint data for the view / notifications
outbound_conflicts = get_outbound_conflicts(form)
button_text = request.POST.get('submit', '')
if button_text == 'Cancel':
@ -534,11 +540,14 @@ def edit(request, acronym, num=None):
#add_session_activity(group,'Session Request was updated',meeting,user)
# send notification
outbound_conflicts = get_outbound_conflicts(form)
session_data = form.cleaned_data.copy() # do not add things to the original cleaned_data
session_data['outbound_conflicts'] = [f"{d['name']}: {d['groups']}" for d in outbound_conflicts]
send_notification(
group,
meeting,
login,
form.cleaned_data,
session_data,
[sf.cleaned_data for sf in form.session_forms.forms_to_keep],
'update',
)

View file

@ -61,7 +61,7 @@
replace: function(new_array){
if (!new_array)
return;
if (!$.isArray(new_array))
if (!Array.isArray(new_array))
new_array = [new_array];
this.clear();
this.push.apply(this, new_array);
@ -176,7 +176,7 @@
},
_resolveDaysOfWeek: function(daysOfWeek){
if (!$.isArray(daysOfWeek))
if (!Array.isArray(daysOfWeek))
daysOfWeek = daysOfWeek.split(/[,\s]*/);
return $.map(daysOfWeek, Number);
},
@ -263,7 +263,7 @@
o.daysOfWeekHighlighted = this._resolveDaysOfWeek(o.daysOfWeekHighlighted||[]);
o.datesDisabled = o.datesDisabled||[];
if (!$.isArray(o.datesDisabled)) {
if (!Array.isArray(o.datesDisabled)) {
o.datesDisabled = o.datesDisabled.split(',');
}
o.datesDisabled = $.map(o.datesDisabled, function(d){
@ -579,7 +579,7 @@
},
setDates: function(){
var args = $.isArray(arguments[0]) ? arguments[0] : arguments;
var args = Array.isArray(arguments[0]) ? arguments[0] : arguments;
this.update.apply(this, args);
this._trigger('changeDate');
this.setValue();
@ -587,7 +587,7 @@
},
setUTCDates: function(){
var args = $.isArray(arguments[0]) ? arguments[0] : arguments;
var args = Array.isArray(arguments[0]) ? arguments[0] : arguments;
this.setDates.apply(this, $.map(args, this._utc_to_local));
return this;
},
@ -1039,7 +1039,7 @@
//Check if uniqueSort exists (supported by jquery >=1.12 and >=2.2)
//Fallback to unique function for older jquery versions
if ($.isFunction($.uniqueSort)) {
if (typeof $.uniqueSort === "function") {
clsName = $.uniqueSort(clsName);
} else {
clsName = $.unique(clsName);

View file

@ -24,11 +24,9 @@ function replace_with_internal(table, internal_table, i) {
}
function field_magic(i, e, fields) {
if ($(e)
.attr("colspan") === undefined &&
(fields[i] == "num" || fields[i] == "count" ||
if (fields[i] == "num" || fields[i] == "count" ||
fields[i] == "percent" || fields[i] == "id" ||
fields[i].endsWith("-num") || fields[i].endsWith("-date"))) {
fields[i].endsWith("-num") || fields[i].endsWith("-date")) {
$(e)
.addClass("text-end");
}
@ -62,12 +60,21 @@ $(document)
// get field classes from first thead row
var fields = $(header_row)
.find("th, td")
.map(function () {
return $(this)
.attr("data-sort") ? $(this)
.toArray()
.map((el) => {
let colspan = parseInt($(el)
.attr("colspan")) || 1;
// create a dense (non-sparse) array
let data_sort = new Array();
for (var i = 0; i < colspan; i++) {
data_sort[i] = "";
}
data_sort[0] = $(el)
.attr("data-sort") ? $(el)
.attr("data-sort") : "";
return data_sort;
})
.toArray();
.flat();
if (fields.length == 0 || !fields.filter(field => field != "")) {
// console.log("No table fields defined, disabling search/sort.");
@ -79,10 +86,9 @@ $(document)
$(header_row)
.children("[data-sort]")
.addClass("sort");
// $(header_row)
// .children("th, td")
// .wrapInner('<span class="tablesorter-th"></span>');
// // .each((i, e) => field_magic(i, e, fields));
$(header_row)
.children("th, td")
.each((i, e) => field_magic(i, e, fields));
if ($(header_row)
.text()

View file

@ -99,6 +99,12 @@
Draft submission
</a>
</li>
<li>
<a class="dropdown-item {% if flavor != 'top' %}text-wrap link-primary{% endif %}"
href="{% url 'ietf.doc.views_search.ad_workload' %}">
IESG dashboard
</a>
</li>
{% if user and user.is_authenticated %}
<li>
<a class="dropdown-item {% if flavor != 'top' %}text-wrap link-primary{% endif %}"

View file

@ -229,9 +229,10 @@
</a>
{% endif %}
{% if liaison.state.slug == 'pending' and can_edit %}
<button class="btn btn-primary" type="submit" name="approved">Approve</button>
<button class="btn btn-primary" type="submit" value="Approve" name="approved">Approve</button>
<button class="btn btn-primary"
type="submit"
value="Mark as Dead"
name="dead">Mark as dead</button>
{% endif %}
{% if liaison.state.slug == 'posted' and user|has_role:"Secretariat" %}
@ -243,6 +244,7 @@
{% if liaison.state.slug == 'dead' and can_edit %}
<button class="btn btn-primary"
type="submit"
value="Resurrect"
name="resurrect">Resurrect</button>
{% endif %}
{% if liaison.state.slug == 'pending' and can_edit or liaison.state.slug == 'dead' and can_edit %}</form>{% endif %}

View file

@ -8,7 +8,7 @@
{% block nomcom_content %}
{% origin %}
<h2 class="mt-3">Nomination status</h2>
<table class="table table-sm table-striped table-hover tablesorter">
<table class="table table-sm table-striped-columns table-hover tablesorter">
<thead class="wrap-anywhere">
<tr>
<th scope="col" data-sort="position">Position</th>
@ -156,7 +156,7 @@
<i class="bi bi-check"></i>
</th>
{% endif %}
<th scope="col" data-sort="nominee" colspan="2">
<th scope="col" data-sort="nominee">
Nominee
</th>
<th scope="col" data-sort="position">
@ -194,9 +194,7 @@
<a href="{% url 'ietf.person.views.profile' email_or_name=np.nominee.name %}">
{{ np.nominee.email.name_and_email }}
</a>
</td>
<td>
<a class="btn btn-primary btn-sm"
<a class="btn btn-primary btn-sm float-end"
href="{% url 'ietf.nomcom.views.view_feedback_nominee' year=year nominee_id=np.nominee.id %}#comment">
View feedback
</a>

Some files were not shown because too many files have changed in this diff Show more