Renamed FlexTable to GridTable (accuracy), removed a lot of test code that was no longer required.
This commit is contained in:
@@ -2,27 +2,16 @@
|
|||||||
<script>
|
<script>
|
||||||
import {Meteor} from "meteor/meteor";
|
import {Meteor} from "meteor/meteor";
|
||||||
import {Route, router} from 'tinro';
|
import {Route, router} from 'tinro';
|
||||||
import {onMount} from 'svelte';
|
|
||||||
import {useTracker} from 'meteor/rdb:svelte-meteor-data';
|
import {useTracker} from 'meteor/rdb:svelte-meteor-data';
|
||||||
import {Roles} from 'meteor/alanning:roles';
|
import {Roles} from 'meteor/alanning:roles';
|
||||||
import Chromebooks from './Chromebooks.svelte';
|
import Chromebooks from './Chromebooks.svelte';
|
||||||
import Users from './Users.svelte';
|
import Users from './Users.svelte';
|
||||||
import TestTable from './TestTable.svelte';
|
|
||||||
import ListUsers from './ListUsers.svelte';
|
|
||||||
import Admin from './Admin.svelte';
|
import Admin from './Admin.svelte';
|
||||||
import Announcer from './Announcer.svelte';
|
import Announcer from './Announcer.svelte';
|
||||||
import {BlazeTemplate} from 'meteor/svelte:blaze-integration';
|
|
||||||
import ServiceConfiguration from "meteor/service-configuration";
|
|
||||||
|
|
||||||
// When the URL changes, run the code... in this case to scroll to the top.
|
// When the URL changes, run the code... in this case to scroll to the top.
|
||||||
router.subscribe(_ => window.scrollTo(0, 0));
|
router.subscribe(_ => window.scrollTo(0, 0));
|
||||||
|
|
||||||
// onMount(async () => {
|
|
||||||
// // Meteor.subscribe('records');
|
|
||||||
// });
|
|
||||||
|
|
||||||
// $: incompleteCount = useTracker(() => Tasks.find({checked: {$ne: true}}).count());
|
|
||||||
|
|
||||||
$: currentUser = useTracker(() => Meteor.user());
|
$: currentUser = useTracker(() => Meteor.user());
|
||||||
$: canManageLaptops = false;
|
$: canManageLaptops = false;
|
||||||
$: isAdmin = false;
|
$: isAdmin = false;
|
||||||
@@ -34,21 +23,6 @@
|
|||||||
isAdmin = user && Roles.userIsInRole(user._id, 'admin', 'global');
|
isAdmin = user && Roles.userIsInRole(user._id, 'admin', 'global');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// const taskStore = Tasks.find({}, {sort: {createdAt: -1}});
|
|
||||||
// $: {
|
|
||||||
// tasks = $taskStore;
|
|
||||||
// if (hideCompleted) {
|
|
||||||
// tasks = tasks.filter(task => !task.checked);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function handleSubmit(event) {
|
|
||||||
// Meteor.call("tasks.insert", newTask);
|
|
||||||
// // Clear form
|
|
||||||
// newTask = "";
|
|
||||||
// }
|
|
||||||
|
|
||||||
function performLogin() {
|
function performLogin() {
|
||||||
//Login style can be "popup" or "redirect". I am not sure we need to request and offline token.
|
//Login style can be "popup" or "redirect". I am not sure we need to request and offline token.
|
||||||
Meteor.loginWithGoogle({loginStyle: "popup", requestOfflineToken: true}, (err) => {
|
Meteor.loginWithGoogle({loginStyle: "popup", requestOfflineToken: true}, (err) => {
|
||||||
@@ -63,7 +37,6 @@
|
|||||||
function performLogout() {
|
function performLogout() {
|
||||||
Meteor.logout();
|
Meteor.logout();
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Announcer/>
|
<Announcer/>
|
||||||
@@ -95,8 +68,6 @@
|
|||||||
{#if isAdmin}
|
{#if isAdmin}
|
||||||
<a href="/admin">Admin</a>
|
<a href="/admin">Admin</a>
|
||||||
{/if}
|
{/if}
|
||||||
<!-- <a href="/TestTable">Test</a>-->
|
|
||||||
<!-- <a href="/ListUsers">List Users</a>-->
|
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,17 +79,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/ListUsers">
|
|
||||||
<!-- <ListUsers/>-->
|
|
||||||
</Route>
|
|
||||||
<Route path="/admin">
|
<Route path="/admin">
|
||||||
{#if isAdmin}
|
{#if isAdmin}
|
||||||
<Admin/>
|
<Admin/>
|
||||||
{/if}
|
{/if}
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/TestTable/*">
|
|
||||||
<!-- <TestTable/>-->
|
|
||||||
</Route>
|
|
||||||
<Route path="/chromebooks/*">
|
<Route path="/chromebooks/*">
|
||||||
{#if canManageLaptops}
|
{#if canManageLaptops}
|
||||||
<Chromebooks/>
|
<Chromebooks/>
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
<script>
|
|
||||||
import {Meteor} from "meteor/meteor";
|
|
||||||
import TestUsers from "./TestUsers.svelte";
|
|
||||||
import {writable} from "svelte/store";
|
|
||||||
|
|
||||||
$: users = Meteor.users.find({});
|
|
||||||
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
key: "_id",
|
|
||||||
title: "ID",
|
|
||||||
value: v => v._id,
|
|
||||||
minWidth: 20,
|
|
||||||
weight: 1,
|
|
||||||
cls: "id",
|
|
||||||
}, {
|
|
||||||
key: "name",
|
|
||||||
title: "Name",
|
|
||||||
value: v => v.profile.name,
|
|
||||||
minWidth: 100,
|
|
||||||
weight: 1,
|
|
||||||
cls: "name",
|
|
||||||
}, {
|
|
||||||
key: "roles",
|
|
||||||
title: "Roles",
|
|
||||||
value: user => {
|
|
||||||
return Roles.getRolesForUser(user, {anyScope: true});
|
|
||||||
},
|
|
||||||
minWidth: 150,
|
|
||||||
weight: 2,
|
|
||||||
cls: "roles",
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const getRowKey = user => {return user._id;}
|
|
||||||
let edited = writable(null);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#await Meteor.subscribe('allUsers')}
|
|
||||||
Loading...
|
|
||||||
{:then allUsers}
|
|
||||||
<TestUsers bind:rows="{users}" columns="{columns}" rowKey="{getRowKey}" bind:edited={edited}>
|
|
||||||
{#if $edited}
|
|
||||||
<input type="text" bind:value={$edited.profile.name}/>
|
|
||||||
{/if}
|
|
||||||
</TestUsers>
|
|
||||||
{:catch error}
|
|
||||||
{error.message}
|
|
||||||
{/await}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
|
|
||||||
/** @type {Array<Object>} */
|
|
||||||
export let columns;
|
|
||||||
/** @type {Array<Object>} */
|
|
||||||
export let rows;
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
// Create a UUID for creating unique instance styles later.
|
|
||||||
const instanceId = Date.now().toString(36) + Math.random().toString(16).slice(2);
|
|
||||||
|
|
||||||
let columnByKey;
|
|
||||||
$: {
|
|
||||||
columnByKey = {};
|
|
||||||
columns.forEach(column => {
|
|
||||||
columnByKey[column.key] = column;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let columnClasses = [];
|
|
||||||
// Create a custom class (style) for each column so we can control the column sizes.
|
|
||||||
$: {
|
|
||||||
// Remove old classes.
|
|
||||||
if(columnClasses && columnClasses.length) {
|
|
||||||
columnClasses.forEach(cls => {
|
|
||||||
try {
|
|
||||||
document.getElementsByTagName('head')[0].removeChild(cls);
|
|
||||||
}
|
|
||||||
catch(e) {console.log(e);}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a unique class for each column so we can manage column sizes.
|
|
||||||
columns.forEach((column, index) => {
|
|
||||||
try {
|
|
||||||
let cls = document.createElement('style');
|
|
||||||
cls.type = 'text/css';
|
|
||||||
column.customClassName = 'svelte-' + instanceId + '-column-' + index;
|
|
||||||
cls.innerHTML = column.customClassName + "{min-width: " + column.width + "; max-width: " + column.width + "; width: " + column.width + ";}";
|
|
||||||
columnClasses[index] = cls;
|
|
||||||
document.getElementsByTagName('head')[0].appendChild(cls);
|
|
||||||
} catch(e) {console.log(e);}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used to create a list of classes for tags.
|
|
||||||
const asStringArray = v => [].concat(v).filter(v => typeof v === "string" && v !== "").join(" ");
|
|
||||||
|
|
||||||
let section;
|
|
||||||
let table;
|
|
||||||
// onMount(async () => {
|
|
||||||
// //let hiddenHeaders = table.querySelectorAll("tbody tr:first-child td");
|
|
||||||
// let hiddenHeaders = table.querySelectorAll("th");
|
|
||||||
// section.querySelectorAll("th").forEach((th, index) => {
|
|
||||||
// hiddenHeaders[index].style.width = th.getBoundingClientRect().width + 'px';
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section bind:this={section}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{#each columns as column}
|
|
||||||
<th class="{column.cls} cell" style="--width-{column.key}: {column.width}; min-width: var(--width-{column.key}); max-width: var(--width-{column.key}); width: var(--width-{column.key});">{column.title}</th>
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
</section>
|
|
||||||
<table bind:this={table}>
|
|
||||||
<thead class="table-head">
|
|
||||||
<tr class="table-head">
|
|
||||||
{#each columns as column}
|
|
||||||
<th class="{column.cls} cell table-head" style="--width-{column.key}: {column.width}; min-width: var(--width-{column.key}); max-width: var(--width-{column.key}); width: var(--width-{column.key});" data-key="{column.key}">{column.title}</th>
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each rows as row}
|
|
||||||
<tr>
|
|
||||||
{#each columns as column}
|
|
||||||
<td class="{column.cls} cell" style="--width-{column.key}: {column.width}; min-width: var(--width-{column.key}); max-width: var(--width-{column.key}); width: var(--width-{column.key});">{column.value(row)}</td>
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
section th:not(:last-child) {
|
|
||||||
border-right: 4px solid gray;
|
|
||||||
}
|
|
||||||
.table-head {
|
|
||||||
visibility: hidden;
|
|
||||||
line-height: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.cell {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,335 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
|
|
||||||
/** @type {Array<Object>} */
|
|
||||||
export let columns;
|
|
||||||
|
|
||||||
/** @type {Array<Object>} */
|
|
||||||
export let rows;
|
|
||||||
|
|
||||||
/** @type {Array<Object>} */
|
|
||||||
export let c_rows;
|
|
||||||
|
|
||||||
/** @type {Array<number>} */
|
|
||||||
export let sortOrders = [1, -1];
|
|
||||||
|
|
||||||
// READ AND WRITE
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
export let sortBy = "";
|
|
||||||
|
|
||||||
/** @type {number} */
|
|
||||||
export let sortOrder = sortOrders?.[0] || 1;
|
|
||||||
|
|
||||||
/** @type {Object} */
|
|
||||||
export let filterSelections = {};
|
|
||||||
|
|
||||||
// expand
|
|
||||||
/** @type {Array.<string|number>} */
|
|
||||||
export let expanded = [];
|
|
||||||
|
|
||||||
// READ ONLY
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
export let expandRowKey = null;
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
export let expandSingle = false;
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
export let iconAsc = "▲";
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
export let iconDesc = "▼";
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
export let iconSortable = "";
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
export let iconExpand = "▼";
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
export let iconExpanded = "▲";
|
|
||||||
|
|
||||||
/** @type {boolean} */
|
|
||||||
export let showExpandIcon = false;
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
export let classNameTable = "";
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
export let classNameThead = "";
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
export let classNameTbody = "";
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
export let classNameSelect = "";
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
export let classNameInput = "";
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
export let classNameRow = "";
|
|
||||||
|
|
||||||
/** @type {string} */
|
|
||||||
export let classNameCell = "";
|
|
||||||
|
|
||||||
/** @type {string} class added to the expanded row*/
|
|
||||||
export let classNameRowExpanded = "";
|
|
||||||
|
|
||||||
/** @type {string} class added to the expanded row*/
|
|
||||||
export let classNameExpandedContent = "";
|
|
||||||
|
|
||||||
/** @type {string} class added to the cell that allows expanding/closing */
|
|
||||||
export let classNameCellExpand = "";
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
|
|
||||||
let sortFunction = () => "";
|
|
||||||
|
|
||||||
// Validation
|
|
||||||
if (!Array.isArray(expanded)) throw "'expanded' needs to be an array";
|
|
||||||
|
|
||||||
let showFilterHeader = columns.some(c => {
|
|
||||||
// check if there are any filter or search headers
|
|
||||||
return c.filterOptions !== undefined || c.searchValue !== undefined;
|
|
||||||
});
|
|
||||||
let filterValues = {};
|
|
||||||
let columnByKey;
|
|
||||||
$: {
|
|
||||||
columnByKey = {};
|
|
||||||
columns.forEach(col => {
|
|
||||||
columnByKey[col.key] = col;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$: colspan = (showExpandIcon ? 1 : 0) + columns.length;
|
|
||||||
|
|
||||||
console.log(rows);
|
|
||||||
|
|
||||||
$: c_rows = rows
|
|
||||||
.filter(r => {
|
|
||||||
// get search and filter results/matches
|
|
||||||
return Object.keys(filterSelections).every(f => {
|
|
||||||
// check search (text input) matches
|
|
||||||
let resSearch =
|
|
||||||
filterSelections[f] === "" ||
|
|
||||||
(columnByKey[f].searchValue &&
|
|
||||||
(columnByKey[f].searchValue(r) + "")
|
|
||||||
.toLocaleLowerCase()
|
|
||||||
.indexOf((filterSelections[f] + "").toLocaleLowerCase()) >= 0);
|
|
||||||
|
|
||||||
// check filter (dropdown) matches
|
|
||||||
let resFilter =
|
|
||||||
resSearch ||
|
|
||||||
filterSelections[f] === undefined ||
|
|
||||||
// default to value() if filterValue() not provided in col
|
|
||||||
filterSelections[f] ===
|
|
||||||
(typeof columnByKey[f].filterValue === "function"
|
|
||||||
? columnByKey[f].filterValue(r)
|
|
||||||
: columnByKey[f].value(r));
|
|
||||||
return resFilter;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.map(r =>
|
|
||||||
Object.assign({}, r, {
|
|
||||||
// internal row property for sort order
|
|
||||||
$sortOn: sortFunction(r),
|
|
||||||
// internal row property for expanded rows
|
|
||||||
$expanded:
|
|
||||||
expandRowKey !== null && expanded.indexOf(r[expandRowKey]) >= 0,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.sort((a, b) => {
|
|
||||||
if (!sortBy) return 0;
|
|
||||||
else if (a.$sortOn > b.$sortOn) return sortOrder;
|
|
||||||
else if (a.$sortOn < b.$sortOn) return -sortOrder;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
const asStringArray = v =>
|
|
||||||
[]
|
|
||||||
.concat(v)
|
|
||||||
.filter(v => typeof v === "string" && v !== "")
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
const calculateFilterValues = () => {
|
|
||||||
filterValues = {};
|
|
||||||
columns.forEach(c => {
|
|
||||||
if (typeof c.filterOptions === "function") {
|
|
||||||
filterValues[c.key] = c.filterOptions(rows);
|
|
||||||
} else if (Array.isArray(c.filterOptions)) {
|
|
||||||
// if array of strings is provided, use it for name and value
|
|
||||||
filterValues[c.key] = c.filterOptions.map(val => ({
|
|
||||||
name: val,
|
|
||||||
value: val,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$: {
|
|
||||||
let col = columnByKey[sortBy];
|
|
||||||
if (
|
|
||||||
col !== undefined &&
|
|
||||||
col.sortable === true &&
|
|
||||||
typeof col.value === "function"
|
|
||||||
) {
|
|
||||||
sortFunction = r => col.value(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: {
|
|
||||||
// if filters are enabled, watch rows and columns
|
|
||||||
if (showFilterHeader && columns && rows) {
|
|
||||||
calculateFilterValues();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateSortOrder = colKey => {
|
|
||||||
return colKey === sortBy
|
|
||||||
? sortOrders[
|
|
||||||
(sortOrders.findIndex(o => o === sortOrder) + 1) % sortOrders.length
|
|
||||||
]
|
|
||||||
: sortOrders[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClickCol = (event, col) => {
|
|
||||||
if (col.sortable) {
|
|
||||||
sortOrder = updateSortOrder(col.key);
|
|
||||||
sortBy = sortOrder ? col.key : undefined;
|
|
||||||
}
|
|
||||||
dispatch("clickCol", { event, col, key: col.key });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClickRow = (event, row) => {
|
|
||||||
dispatch("clickRow", { event, row });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClickExpand = (event, row) => {
|
|
||||||
row.$expanded = !row.$expanded;
|
|
||||||
const keyVal = row[expandRowKey];
|
|
||||||
if (expandSingle && row.$expanded) {
|
|
||||||
expanded = [keyVal];
|
|
||||||
} else if (expandSingle) {
|
|
||||||
expanded = [];
|
|
||||||
} else if (!row.$expanded) {
|
|
||||||
expanded = expanded.filter(r => r != keyVal);
|
|
||||||
} else {
|
|
||||||
expanded = [...expanded, keyVal];
|
|
||||||
}
|
|
||||||
dispatch("clickExpand", { event, row });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClickCell = (event, row, key) => {
|
|
||||||
dispatch("clickCell", { event, row, key });
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<table class={asStringArray(classNameTable)}>
|
|
||||||
<thead class={asStringArray(classNameThead)}>
|
|
||||||
{#if showFilterHeader}
|
|
||||||
<tr>
|
|
||||||
{#each columns as col}
|
|
||||||
<th class={asStringArray([col.headerFilterClass])}>
|
|
||||||
{#if col.searchValue !== undefined}
|
|
||||||
<input
|
|
||||||
bind:value={filterSelections[col.key]}
|
|
||||||
class={asStringArray(classNameInput)}
|
|
||||||
/>
|
|
||||||
{:else if filterValues[col.key] !== undefined}
|
|
||||||
<select
|
|
||||||
bind:value={filterSelections[col.key]}
|
|
||||||
class={asStringArray(classNameSelect)}
|
|
||||||
>
|
|
||||||
<option value={undefined} />
|
|
||||||
{#each filterValues[col.key] as option}
|
|
||||||
<option value={option.value}>{option.name}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
{/if}
|
|
||||||
</th>
|
|
||||||
{/each}
|
|
||||||
{#if showExpandIcon}
|
|
||||||
<th />
|
|
||||||
{/if}
|
|
||||||
</tr>
|
|
||||||
{/if}
|
|
||||||
<slot name="header" {sortOrder} {sortBy}>
|
|
||||||
<tr>
|
|
||||||
{#each columns as col}
|
|
||||||
<th
|
|
||||||
on:click={e => handleClickCol(e, col)}
|
|
||||||
class={asStringArray([
|
|
||||||
col.sortable ? "isSortable" : "",
|
|
||||||
col.headerClass,
|
|
||||||
])}
|
|
||||||
>
|
|
||||||
{col.title}
|
|
||||||
{#if sortBy === col.key}
|
|
||||||
{@html sortOrder === 1 ? iconAsc : iconDesc}
|
|
||||||
{:else if col.sortable}
|
|
||||||
{@html iconSortable}
|
|
||||||
{/if}
|
|
||||||
</th>
|
|
||||||
{/each}
|
|
||||||
{#if showExpandIcon}
|
|
||||||
<th />
|
|
||||||
{/if}
|
|
||||||
</tr>
|
|
||||||
</slot>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody class={asStringArray(classNameTbody)}>
|
|
||||||
{#each c_rows as row, n}
|
|
||||||
<slot name="row" {row} {n}>
|
|
||||||
<tr on:click={e => { handleClickRow(e, row); }} class={asStringArray([classNameRow, row.$expanded && classNameRowExpanded])}>
|
|
||||||
{#each columns as col}
|
|
||||||
<td on:click={e => {handleClickCell(e, row, col.key);}} class={asStringArray([col.class, classNameCell])}>
|
|
||||||
{#if col.renderComponent}
|
|
||||||
<svelte:component
|
|
||||||
this={col.renderComponent.component || col.renderComponent}
|
|
||||||
{...col.renderComponent.props || {}}
|
|
||||||
{row}
|
|
||||||
{col}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
{@html col.renderValue ? col.renderValue(row) : col.value(row)}
|
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
{/each}
|
|
||||||
{#if showExpandIcon}
|
|
||||||
<td on:click={e => handleClickExpand(e, row)} class={asStringArray(["isClickable", classNameCellExpand])}>
|
|
||||||
{@html row.$expanded ? iconExpand : iconExpanded}
|
|
||||||
</td>
|
|
||||||
{/if}
|
|
||||||
</tr>
|
|
||||||
{#if row.$expanded}
|
|
||||||
<tr class={asStringArray(classNameExpandedContent)}>
|
|
||||||
<td {colspan}>
|
|
||||||
<slot name="expanded" {row} {n} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/if}
|
|
||||||
</slot>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.isSortable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.isClickable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr th select {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
|
|
||||||
<script>
|
|
||||||
import {Route, router, meta} from 'tinro';
|
|
||||||
import {Meteor} from "meteor/meteor";
|
|
||||||
import FlexTable from "./FlexTable.svelte";
|
|
||||||
import {useTracker} from "meteor/rdb:svelte-meteor-data";
|
|
||||||
import {writable} from "svelte/store";
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
key: "_id",
|
|
||||||
title: "ID",
|
|
||||||
value: v => v._id,
|
|
||||||
minWidth: 20,
|
|
||||||
weight: 1,
|
|
||||||
}, {
|
|
||||||
key: "text",
|
|
||||||
title: "Text",
|
|
||||||
value: v => v.text,
|
|
||||||
minWidth: 100,
|
|
||||||
weight: 1,
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
let rows = writable([
|
|
||||||
{
|
|
||||||
_id: "1",
|
|
||||||
text: "A"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: "2",
|
|
||||||
text: "B"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: "3",
|
|
||||||
text: "C"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: "4",
|
|
||||||
text: "D"
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const getRowKey = (row) => row._id;
|
|
||||||
let edited = writable(null);
|
|
||||||
let text = "";
|
|
||||||
const addRow = () => {
|
|
||||||
$rows[$rows.length] = {_id: "" + ($rows.length + 1), text};
|
|
||||||
text = "";
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<Route path="/">
|
|
||||||
<div class="container">
|
|
||||||
<div class="row col-12 table">
|
|
||||||
<FlexTable columns="{columns}" bind:rows="{$rows}" rowKey="{getRowKey}" edited="{edited}">
|
|
||||||
My Editor....
|
|
||||||
</FlexTable>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container">
|
|
||||||
<div class="row col-12">
|
|
||||||
<input type="text" bind:value={text}/>
|
|
||||||
<button type="button" on:click={addRow}>Add</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Route>
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
<script>
|
|
||||||
export let rows;
|
|
||||||
export let columns;
|
|
||||||
export let rowKey;
|
|
||||||
export let edited;
|
|
||||||
|
|
||||||
// Setup a width for each column.
|
|
||||||
columns.forEach(column => {
|
|
||||||
let min = column.minWidth ? Math.max(10, column.minWidth) : 10;
|
|
||||||
let weight = column.weight ? Math.max(1, column.weight) : 1;
|
|
||||||
column.width = 'minmax(' + min + 'px, ' + weight + 'fr)';
|
|
||||||
});
|
|
||||||
let gridTemplateColumns = columns.map(({width}) => width).join(' ');
|
|
||||||
|
|
||||||
let headerBeingResized = null;
|
|
||||||
let horizontalScrollOffset = 0;
|
|
||||||
const initResize = ({target}) => {
|
|
||||||
headerBeingResized = target.parentNode;
|
|
||||||
window.addEventListener('mousemove', onMouseMove);
|
|
||||||
window.addEventListener('mouseup', completeResize);
|
|
||||||
headerBeingResized.classList.add('header--being-resized');
|
|
||||||
};
|
|
||||||
const completeResize = () => {
|
|
||||||
window.removeEventListener('mousemove', onMouseMove);
|
|
||||||
window.removeEventListener('mouseup', completeResize);
|
|
||||||
headerBeingResized.classList.remove('header--being-resized');
|
|
||||||
headerBeingResized = null;
|
|
||||||
};
|
|
||||||
const onMouseMove = e => {
|
|
||||||
try {
|
|
||||||
// Calculate the desired width.
|
|
||||||
horizontalScrollOffset = document.documentElement.scrollLeft;
|
|
||||||
let parentX = Math.round(headerBeingResized.getBoundingClientRect().x);
|
|
||||||
const width = horizontalScrollOffset + (e.clientX - parentX);
|
|
||||||
// Update the column object with the new size value.
|
|
||||||
const column = columns.find(({element}) => element === headerBeingResized);
|
|
||||||
column.width = Math.max(column.minWidth, width) + "px";
|
|
||||||
// Ensure all the column widths are converted to fixed sizes.
|
|
||||||
columns.forEach((column, index) => {
|
|
||||||
if((index < columns.length - 1) && (column.width.startsWith('minmax'))) {
|
|
||||||
column.width = parseInt(column.element.clientWidth, 10) + 'px';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Render the new column sizes.
|
|
||||||
gridTemplateColumns = columns.map(({width}) => width).join(' ');
|
|
||||||
} catch(e) {console.log(e);}
|
|
||||||
}
|
|
||||||
|
|
||||||
let selectedRowElement = null;
|
|
||||||
const selectRow = (e, row) => {
|
|
||||||
let element = e.target;
|
|
||||||
|
|
||||||
while(element && element.nodeName !== "TR") element = element.parentNode;
|
|
||||||
|
|
||||||
if(selectedRowElement) {
|
|
||||||
selectedRowElement.classList.remove('selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedRowElement = element;
|
|
||||||
element.classList.add('selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
let editorContainer;
|
|
||||||
const editRow = (e, row) => {
|
|
||||||
let element = e.target;
|
|
||||||
while(element && element.nodeName !== "TR") element = element.parentNode;
|
|
||||||
let editor = element.querySelector('.editor');
|
|
||||||
|
|
||||||
// Save the edited row so the editor has access to it.
|
|
||||||
$edited = row;
|
|
||||||
|
|
||||||
if(editor) {
|
|
||||||
editor.appendChild(editorContainer);
|
|
||||||
}
|
|
||||||
editorContainer.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div bind:this={editorContainer} class="hidden"><slot>Slot</slot></div>
|
|
||||||
<table style="--grid-template-columns: {gridTemplateColumns}">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{#each columns as column}
|
|
||||||
<th bind:this={column.element}>{column.title} <span class="resize-handle" on:mousedown={initResize}></span></th>
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each $rows as row (rowKey(row))}
|
|
||||||
<!-- data-key="{rowKey(row)}"-->
|
|
||||||
<tr class:hidden={row === $edited} on:mousedown={(e) => selectRow(e, row)} on:dblclick={(e) => editRow(e, row)}>
|
|
||||||
{#each columns as column}
|
|
||||||
<td>{column.value(row)}</td>
|
|
||||||
{/each}
|
|
||||||
<td class="editor"></td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<button on:click={() => {$edited = null}} type="button">Stop Editing</button>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
table {
|
|
||||||
width: auto;
|
|
||||||
-webkit-box-flex: 1;
|
|
||||||
flex: 1;
|
|
||||||
display: grid;
|
|
||||||
border-collapse: collapse;
|
|
||||||
grid-template-columns: var(--grid-template-columns);
|
|
||||||
}
|
|
||||||
thead, tbody, tr {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
th, td {
|
|
||||||
padding: 15px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
th {
|
|
||||||
position: -webkit-sticky;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background: #5cb85c;
|
|
||||||
text-align: left;
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: white;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
th:last-child {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
.resize-handle {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: black;
|
|
||||||
opacity: 0;
|
|
||||||
width: 3px;
|
|
||||||
cursor: col-resize;
|
|
||||||
}
|
|
||||||
th:last-child .resize-handle {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.resize-handle:hover, .header--being-resized .resize-handle {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
th:hover .resize-handle {
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
padding-top: 10px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
color: #808080;
|
|
||||||
}
|
|
||||||
tr:nth-child(even) {
|
|
||||||
background: #f8f6ff;
|
|
||||||
}
|
|
||||||
:global(.selected), :global(.selected) > td {
|
|
||||||
background-color: yellow;
|
|
||||||
}
|
|
||||||
.editor {
|
|
||||||
grid-column: 1 / 4;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
/*:global(td.hidden) {*/
|
|
||||||
/* display: none;*/
|
|
||||||
/*}*/
|
|
||||||
:global(tr.hidden) > td:not(.editor) {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
:global(tr.hidden) > td.editor {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
:global(div.hidden) {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import {Route, router, meta} from 'tinro';
|
import {Route, router, meta} from 'tinro';
|
||||||
import {Meteor} from "meteor/meteor";
|
import {Meteor} from "meteor/meteor";
|
||||||
import FlexTable from "./FlexTable.svelte";
|
import GridTable from "./GridTable.svelte";
|
||||||
import {useTracker} from "meteor/rdb:svelte-meteor-data";
|
import {useTracker} from "meteor/rdb:svelte-meteor-data";
|
||||||
import {writable} from "svelte/store";
|
import {writable} from "svelte/store";
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
{#await Promise.all([Meteor.subscribe('allUsers'), Meteor.subscribe('allRoleAssignments')])}
|
{#await Promise.all([Meteor.subscribe('allUsers'), Meteor.subscribe('allRoleAssignments')])}
|
||||||
Loading...
|
Loading...
|
||||||
{:then allUsers}
|
{:then allUsers}
|
||||||
<FlexTable bind:rows={rows} columns="{columns}" rowKey="{getRowKey}" bind:edited="{edited}">
|
<GridTable bind:rows={rows} columns="{columns}" rowKey="{getRowKey}" bind:edited="{edited}">
|
||||||
{#if editedPermissions}
|
{#if editedPermissions}
|
||||||
<div class="editorContainer">
|
<div class="editorContainer">
|
||||||
<label style="grid-column: 1/4; font-weight: 800; border-bottom: 2px solid #888; margin-bottom: 0.5rem">{$edited.profile.name}</label>
|
<label style="grid-column: 1/4; font-weight: 800; border-bottom: 2px solid #888; margin-bottom: 0.5rem">{$edited.profile.name}</label>
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
<button type="button" style="grid-column: 3/3;" class="button reject-button" on:click={rejectChanges}> </button>
|
<button type="button" style="grid-column: 3/3;" class="button reject-button" on:click={rejectChanges}> </button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</FlexTable>
|
</GridTable>
|
||||||
<button type="button" on:click="{changeColWidth}">Change Width</button>
|
<button type="button" on:click="{changeColWidth}">Change Width</button>
|
||||||
{:catch error}
|
{:catch error}
|
||||||
{error.message}
|
{error.message}
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
|
|
||||||
<script>
|
|
||||||
import {Meteor} from "meteor/meteor";
|
|
||||||
import {Route, router} from 'tinro';
|
|
||||||
import {useTracker} from 'meteor/rdb:svelte-meteor-data';
|
|
||||||
import {Roles} from 'meteor/alanning:roles';
|
|
||||||
import Chromebooks from './Chromebooks.svelte';
|
|
||||||
import Users from './Users.svelte';
|
|
||||||
import ListUsers from './ListUsers.svelte';
|
|
||||||
import Admin from './Admin.svelte';
|
|
||||||
import Announcer from './Announcer.svelte';
|
|
||||||
|
|
||||||
// When the URL changes, run the code... in this case to scroll to the top.
|
|
||||||
router.subscribe(_ => window.scrollTo(0, 0));
|
|
||||||
|
|
||||||
$: currentUser = useTracker(() => Meteor.user());
|
|
||||||
$: canManageLaptops = false;
|
|
||||||
$: isAdmin = false;
|
|
||||||
|
|
||||||
Tracker.autorun(() => {
|
|
||||||
// For some reason currentUser is always null here, and is not reactive (user changes and this does not get re-called).
|
|
||||||
let user = Meteor.user();
|
|
||||||
canManageLaptops = user && Roles.userIsInRole(user._id, 'laptop-management', 'global');
|
|
||||||
isAdmin = user && Roles.userIsInRole(user._id, 'admin', 'global');
|
|
||||||
});
|
|
||||||
function performLogin() {
|
|
||||||
//Login style can be "popup" or "redirect". I am not sure we need to request and offline token.
|
|
||||||
Meteor.loginWithGoogle({loginStyle: "popup", requestOfflineToken: true}, (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.log(err);
|
|
||||||
} else {
|
|
||||||
//console.log("Logged in");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
function performLogout() {
|
|
||||||
Meteor.logout();
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Announcer/>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<header class="row">
|
|
||||||
<div class="col-12 logoContainer">
|
|
||||||
<img class="logo" src="/images/logo.svg"/>
|
|
||||||
<div class="login">
|
|
||||||
{#if !$currentUser}
|
|
||||||
<button type="button" role="button" on:click={performLogin}>Login</button>
|
|
||||||
{:else}
|
|
||||||
<button type="button" role="button" on:click={performLogout}>Logout</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 center" style="margin-bottom: 0"><h1 style="margin-bottom: 0">District Central</h1></div>
|
|
||||||
<div class="col-12 center">
|
|
||||||
<div class="nav-separator"></div>
|
|
||||||
</div>
|
|
||||||
<nav class="col-12 center">
|
|
||||||
<a href="/">Home</a>
|
|
||||||
{#if canManageLaptops}
|
|
||||||
<a href="/chromebooks">Chromebooks</a>
|
|
||||||
{/if}
|
|
||||||
{#if canManageLaptops}
|
|
||||||
<a href="/users">Users</a>
|
|
||||||
{/if}
|
|
||||||
{#if isAdmin}
|
|
||||||
<a href="/admin">Admin</a>
|
|
||||||
{/if}
|
|
||||||
<!-- <a href="/TestTable">Test</a>-->
|
|
||||||
<!-- <a href="/ListUsers">List Users</a>-->
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Route path="/">
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
TODO: Some statistics and such.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Route>
|
|
||||||
<Route path="/ListUsers">
|
|
||||||
<ListUsers/>
|
|
||||||
</Route>
|
|
||||||
<Route path="/admin">
|
|
||||||
{#if isAdmin}
|
|
||||||
<Admin/>
|
|
||||||
{/if}
|
|
||||||
</Route>
|
|
||||||
<Route path="/chromebooks/*">
|
|
||||||
{#if canManageLaptops}
|
|
||||||
<Chromebooks/>
|
|
||||||
{:else}
|
|
||||||
<!-- User not authorized to use this UI. Don't render anything because it is likely the user is still loading and will have access in a moment. -->
|
|
||||||
{/if}
|
|
||||||
</Route>
|
|
||||||
<Route path="/users/*">
|
|
||||||
{#if isAdmin}
|
|
||||||
<Users/>
|
|
||||||
{:else}
|
|
||||||
<!-- User not authorized to use this UI. Don't render anything because it is likely the user is still loading and will have access in a moment. -->
|
|
||||||
{/if}
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
...
|
|
||||||
</style>
|
|
||||||
Reference in New Issue
Block a user