Dependency refactor #17

Merged
nomadjimbob merged 155 commits from dependency-refactor into main 2023-02-27 12:30:57 +00:00
21 changed files with 382 additions and 405 deletions
Showing only changes of commit 7de134683d - Show all commits

View File

@@ -1,34 +0,0 @@
import _ from "lodash";
window._ = _;
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
// import axios from 'axios';
// window.axios = axios;
// window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo';
// import Pusher from 'pusher-js';
// window.Pusher = Pusher;
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: import.meta.env.VITE_PUSHER_APP_KEY,
// wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
// wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
// wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
// enabledTransports: ['ws', 'wss'],
// });

View File

@@ -27,7 +27,7 @@
<script setup lang="ts">
import Trix from "trix";
import { ref, watch, computed, onUnmounted } from "vue";
import { arrayIncludesMatchBasic } from "../helpers/common";
import { arrayHasBasicMatch } from "../helpers/array";
import DialogMedia from "./dialogs/SMDialogMedia.vue";
import { openDialog } from "vue3-promise-dialog";
@@ -154,7 +154,7 @@ const emitEditorState = (value) => {
const emitFileAccept = (event) => {
if (props.mimeTypes) {
if (!arrayIncludesMatchBasic(props.mimeTypes, event.file.type)) {
if (!arrayHasBasicMatch(props.mimeTypes, event.file.type)) {
window.alert("That file type is not supported");
event.preventDefault();
return;

View File

@@ -20,7 +20,7 @@ const parsedContent = computed(() => {
`<a ([^>]*?)href="${import.meta.env.APP_URL}(.*?>.*?)</a>`,
"ig"
);
html = props.html.replaceAll(regex, '<router-link $1to="$2</router-link>');
html = props.html.replace(regex, '<router-link $1to="$2</router-link>');
return {
template: `<div class="content">${html}</div>`,

View File

@@ -39,12 +39,8 @@
<script setup lang="ts">
import { onMounted, computed, ref } from "vue";
import {
excerpt,
isUUID,
replaceHtmlEntites,
stripHtmlTags,
} from "../helpers/common";
import { isUUID } from "../helpers/types";
import { excerpt, replaceHtmlEntites, stripHtmlTags } from "../helpers/string";
import { format } from "date-fns";
import { api } from "../helpers/api";
import { imageLoad } from "../helpers/image";

View File

@@ -80,7 +80,6 @@ import SMFormFooter from "../SMFormFooter.vue";
import SMDialog from "../SMDialog.vue";
import SMMessage from "../SMMessage.vue";
import SMModal from "../SMModal.vue";
import { toParamString } from "../../helpers/common";
import { api } from "../../helpers/api";
const uploader = ref(null);
@@ -134,10 +133,13 @@ const handleLoad = async () => {
params.limit = perPage.value;
// params.fields = "url";
let res = await api.get(`/media${toParamString(params)}`);
let res = await api.get({
url: "/media",
params: params,
});
totalItems.value = res.json.total;
mediaItems.value = res.json.media;
totalItems.value = res.data.total;
mediaItems.value = res.data.media;
} catch (error) {
if (error.status == 404) {
formMessage.type = "primary";
@@ -145,7 +147,7 @@ const handleLoad = async () => {
formMessage.message = "No media items found";
} else {
formMessage.message =
error?.json?.message || "An unexpected error occurred";
error?.data?.message || "An unexpected error occurred";
}
}
};

View File

@@ -0,0 +1,24 @@
/**
* Test if array has a match using basic search (* means anything)
*
* @param {Array<string>} arr The array to search.
* @param {string} str The string to find.
* @returns {boolean} if the array has the string.
*/
export const arrayHasBasicMatch = (
arr: Array<string>,
str: string
): boolean => {
let matches = false;
arr.every((elem) => {
elem = elem.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
const regex = new RegExp("^" + elem.replace("*", ".*?") + "$", "i");
if (str.match(regex)) {
matches = true;
}
return !matches;
});
return matches;
};

View File

@@ -1,318 +0,0 @@
const transitionEndEventName = () => {
var i,
undefined,
el = document.createElement("div"),
transitions = {
transition: "transitionend",
OTransition: "otransitionend",
MozTransition: "transitionend",
WebkitTransition: "webkitTransitionEnd",
};
for (i in transitions) {
if (transitions.hasOwnProperty(i) && el.style[i] !== undefined) {
return transitions[i];
}
}
return null;
};
const waitForElementRender = (elem) => {
return new Promise((resolve) => {
if (document.contains(elem.value)) {
return resolve(elem.value);
}
const MutationObserver =
window.MutationObserver ||
window.WebKitMutationObserver ||
window.MozMutationObserver;
const observer = new MutationObserver((mutations) => {
if (document.contains(elem.value)) {
resolve(elem.value);
observer.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
});
};
const transitionEnter = (elem, transition) => {
waitForElementRender(elem).then((e) => {
window.setTimeout(() => {
e.classList.replace(
transition + "-enter-from",
transition + "-enter-active"
);
const transitionName = transitionEndEventName();
e.addEventListener(
transitionName,
() => {
e.classList.replace(
transition + "-enter-active",
transition + "-enter-to"
);
},
false
);
}, 1);
});
};
const transitionLeave = (elem, transition, callback = null) => {
elem.value.classList.remove(transition + "-enter-to");
elem.value.classList.add(transition + "-leave-from");
window.setTimeout(() => {
elem.value.classList.replace(
transition + "-leave-from",
transition + "-leave-active"
);
const transitionName = transitionEndEventName();
elem.value.addEventListener(
transitionName,
() => {
elem.value.classList.replace(
transition + "-leave-active",
transition + "-leave-to"
);
if (callback) {
callback();
}
},
false
);
}, 1);
};
export const monthString = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
export const fullMonthString = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
/**
*
* @param target
*/
export function isBool(target) {
return typeof target === "boolean";
}
/**
*
* @param target
*/
export function isNumber(target) {
return typeof target === "number";
}
/**
*
* @param target
*/
export function isObject(target) {
return typeof target === "object" && target !== null;
}
/**
*
* @param target
*/
export function isString(target) {
return typeof target === "string" && target !== null;
}
/**
*
* @param target
* @param def
*/
export function parseErrorType(
target,
def = "An unknown error occurred. Please try again later."
) {
if (target.response?.message) {
return target.response.message;
} else if (target instanceof Error) {
target.message;
} else if (isString(err)) {
return target;
}
return def;
}
export const buildUrlQuery = (url, query) => {
let s = "";
if (Object.keys(query).length > 0) {
s = "?";
}
s += Object.keys(query)
.map((key) => key + "=" + query[key])
.join("&");
return url + s;
};
export const toParamString = (obj, q = true) => {
let s = "";
if (q && Object.keys(obj).length > 0) {
s = "?";
}
s += Object.keys(obj)
.map((key) => key + "=" + obj[key])
.join("&");
return s;
};
export const getLocale = () => {
return (
navigator.userLanguage ||
(navigator.languages &&
navigator.languages.length &&
navigator.languages[0]) ||
navigator.language ||
navigator.browserLanguage ||
navigator.systemLanguage ||
"en"
);
};
export const debounce = (fn, delay) => {
var timeoutID = null;
return function () {
clearTimeout(timeoutID);
var args = arguments;
var that = this;
timeoutID = setTimeout(function () {
fn.apply(that, args);
}, delay);
};
};
export const bytesReadable = (bytes) => {
if (isNaN(bytes)) {
return "0 Bytes";
}
if (Math.abs(bytes) < 1024) {
return bytes + " Bytes";
}
const units = ["KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
let u = -1;
const r = 10 ** 1;
do {
bytes /= 1000;
++u;
} while (
Math.round(Math.abs(bytes) * r) / r >= 1000 &&
u < units.length - 1
);
return bytes.toFixed(1) + " " + units[u];
};
export const arrayIncludesMatchBasic = (arr, str) => {
let matches = false;
arr.every((elem) => {
elem = elem.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
let regex = new RegExp("^" + elem.replace("*", ".*?") + "$", "i");
if (str.match(regex)) {
matches = true;
}
return !matches;
});
return matches;
};
export const excerpt = (txt, maxLen = 150, strip = true) => {
if (strip) {
txt = stripHtmlTags(replaceHtmlEntites(txt));
}
let txtPieces = txt.split(" ");
let excerptPieces = [];
let curLen = 0;
txtPieces.every((itm) => {
if (curLen + itm.length >= maxLen) {
return false;
}
excerptPieces.push(itm);
curLen += itm.length + 1;
return true;
});
return excerptPieces.join(" ") + (curLen < txt.length ? "..." : "");
};
export const stripHtmlTags = (txt) => {
txt = txt.replace(/<(p|br)([ /]*?>|[ /]+.*?>)/g, " ");
return txt.replace(/<[a-zA-Z/][^>]+(>|$)/g, "");
};
export const replaceHtmlEntites = (txt) => {
var translate_re = /&(nbsp|amp|quot|lt|gt);/g;
var translate = {
nbsp: " ",
amp: "&",
quot: '"',
lt: "<",
gt: ">",
};
return txt.replace(translate_re, function (match, entity) {
return translate[entity];
});
};
export const isUUID = (uuid) => {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
uuid
);
};
export {
transitionEndEventName,
waitForElementRender,
transitionEnter,
transitionLeave,
};

View File

@@ -0,0 +1,27 @@
type DebounceCallback = () => void;
type DebounceResult = (...args: unknown[]) => void;
/**
* Call a function after a delay once.
*
* @param {Function} fn The function to call.
* @param {number} delay The delay before calling function.
* @returns {void}
*/
export const debounce = (
fn: DebounceCallback,
delay: number
): DebounceResult => {
let timeoutID: NodeJS.Timeout | null = null;
return (...args) => {
if (timeoutID != null) {
clearTimeout(timeoutID);
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
timeoutID = setTimeout(function () {
fn.apply(that, args);
}, delay);
};
};

View File

@@ -1,8 +0,0 @@
export const toTitleCase = (str) => {
return str.replace(/\w\S*/g, function (txt) {
return (
txt.charAt(0).toUpperCase() +
txt.substr(1).replaceAll("_", " ").toLowerCase()
);
});
};

View File

@@ -0,0 +1,81 @@
/**
* Transforms a string to title case.
*
* @param {string} str The string to transform.
* @returns {string} A string transformed to title case.
*/
export const toTitleCase = (str: string): string => {
return str.replace(/\w\S*/g, function (txt) {
return (
txt.charAt(0).toUpperCase() +
txt.substring(1).replace(/_/g, " ").toLowerCase()
);
});
};
/**
* Convert a string to a excerpt.
*
* @param {string} txt The text to convert.
* @param {number} maxLen (optional) The maximum length of the excerpt.
* @param {boolean} strip (optional) Strip HTML tags from the text.
* @param stripHtml
* @returns {string} The excerpt.
*/
export const excerpt = (
txt: string,
maxLen: number = 150,
stripHtml: boolean = true
): string => {
if (stripHtml) {
txt = stripHtmlTags(replaceHtmlEntites(txt));
}
const txtPieces = txt.split(" ");
const excerptPieces: string[] = [];
let curLen = 0;
txtPieces.every((itm) => {
if (curLen + itm.length >= maxLen) {
return false;
}
excerptPieces.push(itm);
curLen += itm.length + 1;
return true;
});
return excerptPieces.join(" ") + (curLen < txt.length ? "..." : "");
};
/**
* String HTML tags from text.
*
* @param {string} txt The text to strip tags.
* @returns {string} The stripped text.
*/
export const stripHtmlTags = (txt: string): string => {
txt = txt.replace(/<(p|br)([ /]*?>|[ /]+.*?>)/g, " ");
return txt.replace(/<[a-zA-Z/][^>]+(>|$)/g, "");
};
/**
* Replace HTML entities with real characters.
*
* @param {string} txt The text to transform.
* @returns {string} Transformed text
*/
export const replaceHtmlEntites = (txt: string): string => {
const translate_re = /&(nbsp|amp|quot|lt|gt);/g;
const translate = {
nbsp: " ",
amp: "&",
quot: '"',
lt: "<",
gt: ">",
};
return txt.replace(translate_re, function (match, entity) {
return translate[entity];
});
};

View File

@@ -0,0 +1,127 @@
import { Ref } from "vue";
/**
* Return the browser transiton end name.
*
* @returns {string} The browser transition end name.
*/
const transitionEndEventName = (): string => {
const el = document.createElement("div"),
transitions: Record<string, string> = {
transition: "transitionend",
OTransition: "otransitionend",
MozTransition: "transitionend",
WebkitTransition: "webkitTransitionEnd",
};
for (const i in transitions) {
if (
Object.prototype.hasOwnProperty.call(transitions, i) &&
el.style[i] !== undefined
) {
return transitions[i];
}
}
return "";
};
/**
* Wait for the element to render as Promise
*
* @param elem The
* @returns
*/
const waitForElementRender = (elem: Ref): Promise<HTMLElement> => {
return new Promise((resolve) => {
if (document.contains(elem.value)) {
return resolve(elem.value as HTMLElement);
}
/* eslint-disable @typescript-eslint/no-explicit-any */
const MutationObserver =
window.MutationObserver ||
(window as any).WebKitMutationObserver ||
(window as any).MozMutationObserver;
/* eslint-enable @typescript-eslint/no-explicit-any */
const observer = new MutationObserver(() => {
if (document.contains(elem.value)) {
resolve(elem.value);
observer.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
});
};
/**
* Run the enter transition on a element.
*
* @param {Ref} elem The element to run the enter transition.
* @param {string} transition The transition name.
* @returns {void}
*/
export const transitionEnter = (elem: Ref, transition: string): void => {
waitForElementRender(elem).then((e: HTMLElement) => {
window.setTimeout(() => {
e.classList.replace(
transition + "-enter-from",
transition + "-enter-active"
);
const transitionName = transitionEndEventName();
e.addEventListener(
transitionName,
() => {
e.classList.replace(
transition + "-enter-active",
transition + "-enter-to"
);
},
false
);
}, 1);
});
};
/**
* Run the exit transition on a element then call a callback.
*
* @param {Ref} elem The element to run the enter transition.
* @param {string} transition The transition name.
* @param {TransitionLeaveCallback|null} callback The callback to run after the transition finishes.
* @returns {void}
*/
type TransitionLeaveCallback = () => void;
export const transitionLeave = (
elem: Ref,
transition: string,
callback: TransitionLeaveCallback | null = null
): void => {
elem.value.classList.remove(transition + "-enter-to");
elem.value.classList.add(transition + "-leave-from");
window.setTimeout(() => {
elem.value.classList.replace(
transition + "-leave-from",
transition + "-leave-active"
);
const transitionName = transitionEndEventName();
elem.value.addEventListener(
transitionName,
() => {
elem.value.classList.replace(
transition + "-leave-active",
transition + "-leave-to"
);
if (callback) {
callback();
}
},
false
);
}, 1);
};

View File

@@ -0,0 +1,81 @@
/**
* Test if target is a boolean
*
* @param {unknown} target The varible to test
* @returns {boolean} If the varible is a boolean type
*/
export function isBool(target: unknown): boolean {
return typeof target === "boolean";
}
/**
* Test if target is a number
*
* @param {unknown} target The varible to test
* @returns {boolean} If the varible is a number type
*/
export function isNumber(target: unknown): boolean {
return typeof target === "number";
}
/**
* Test if target is an object
*
* @param {unknown} target The varible to test
* @returns {boolean} If the varible is a object type
*/
export function isObject(target: unknown): boolean {
return typeof target === "object" && target !== null;
}
/**
* Test if target is a string
*
* @param {unknown} target The varible to test
* @returns {boolean} If the varible is a string type
*/
export function isString(target: unknown): boolean {
return typeof target === "string" && target !== null;
}
/**
* Test if target is a UUID
*
* @param {string} uuid The variable to test
* @returns {boolean} If the varible is a UUID
*/
export const isUUID = (uuid: string): boolean => {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
uuid
);
};
/**
* Convert bytes to a human readable string.
*
* @param {number} bytes The bytes to convert.
* @returns {string} The bytes in human readable string.
*/
export const bytesReadable = (bytes: number): string => {
if (isNaN(bytes)) {
return "0 Bytes";
}
if (Math.abs(bytes) < 1024) {
return bytes + " Bytes";
}
const units = ["KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
let u = -1;
const r = 10 ** 1;
do {
bytes /= 1000;
++u;
} while (
Math.round(Math.abs(bytes) * r) / r >= 1000 &&
u < units.length - 1
);
return bytes.toFixed(1) + " " + units[u];
};

View File

@@ -1,5 +1,5 @@
import { SMDate } from "./datetime";
import { bytesReadable } from "../helpers/common";
import { bytesReadable } from "../helpers/types";
export interface ValidationObject {
validate: (value: string) => ValidationResult;

View File

@@ -7,8 +7,8 @@
</template>
<script setup lang="ts">
import { ref, onBeforeMount } from "vue";
import { transitionEnter, transitionLeave } from "../helpers/common";
import { ref } from "vue";
import { transitionEnter, transitionLeave } from "../helpers/transition";
const root = ref(null);
const classes = ref(["mdialog-mask", "fade-enter-from"]);

View File

@@ -124,7 +124,7 @@
<script setup lang="ts">
import { reactive, ref } from "vue";
import { excerpt } from "../helpers/common";
import { excerpt } from "../helpers/string";
import { SMDate } from "../helpers/datetime";
import SMInput from "../components/SMInput.vue";
import SMButton from "../components/SMButton.vue";

View File

@@ -26,7 +26,6 @@
<script setup lang="ts">
import { ref, computed } from "vue";
import { useRoute } from "vue-router";
import { fullMonthString } from "../helpers/common";
import { SMDate } from "../helpers/datetime";
import { useApplicationStore } from "../store/ApplicationStore";
import { api } from "../helpers/api";
@@ -92,14 +91,7 @@ const loadData = async () => {
};
const formattedPublishAt = (dateStr) => {
const date = new Date(Date.parse(dateStr));
return (
fullMonthString[date.getMonth()] +
" " +
date.getDate() +
", " +
date.getFullYear()
);
return new SMDate(dateStr, { format: "yMd" }).format("MMMM d, yyyy");
};
const formattedContent = computed(() => {
@@ -109,7 +101,7 @@ const formattedContent = computed(() => {
`<a ([^>]*?)href="${import.meta.env.APP_URL}(.*?>.*?)</a>`,
"ig"
);
html = html.replaceAll(regex, '<router-link $1to="$2</router-link>');
html = html.replace(regex, '<router-link $1to="$2</router-link>');
}
return {

View File

@@ -88,7 +88,7 @@ import {
Required,
} from "../helpers/validate";
import { debounce } from "../helpers/common";
import { debounce } from "../helpers/debounce";
import { useReCaptcha } from "vue-recaptcha-v3";
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();

View File

@@ -52,13 +52,13 @@
import { ref, watch, reactive } from "vue";
import EasyDataTable from "vue3-easy-data-table";
import { api } from "../../helpers/api";
import { relativeDate, timestampUtcToLocal } from "../../helpers/datetime";
import { SMDate } from "../../helpers/datetime";
import { useRouter } from "vue-router";
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
import { openDialog } from "vue3-promise-dialog";
import SMToolbar from "../../components/SMToolbar.vue";
import SMButton from "../../components/SMButton.vue";
import { debounce } from "../../helpers/common";
import { debounce } from "../../helpers/debounce";
import SMHeading from "../../components/SMHeading.vue";
import SMMessage from "../../components/SMMessage.vue";
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
@@ -128,13 +128,22 @@ const loadFromServer = async () => {
items.value.forEach((row) => {
if (row.start_at !== "undefined") {
row.start_at = relativeDate(timestampUtcToLocal(row.start_at));
row.start_at = new SMDate(row.start_at, {
format: "ymd",
utc: true,
}).relative();
}
if (row.created_at !== "undefined") {
row.created_at = relativeDate(row.created_at);
row.created_at = new SMDate(row.creative_at, {
format: "ymd",
utc: true,
}).relative();
}
if (row.updated_at !== "undefined") {
row.updated_at = relativeDate(row.updated_at);
row.updated_at = new SMDate(row.updated_at, {
format: "ymd",
utc: true,
}).relative();
}
});

View File

@@ -72,7 +72,7 @@ import { api } from "../../helpers/api";
import { FormObject, FormControl } from "../../helpers/form";
import { And, Required, FileSize } from "../../helpers/validate";
import { useRoute } from "vue-router";
import { bytesReadable } from "../../helpers/common";
import { bytesReadable } from "../../helpers/types";
import { useRouter } from "vue-router";
const router = useRouter();

View File

@@ -61,7 +61,8 @@ import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
import { openDialog } from "vue3-promise-dialog";
import SMToolbar from "../../components/SMToolbar.vue";
import SMButton from "../../components/SMButton.vue";
import { debounce, parseErrorType, bytesReadable } from "../../helpers/common";
import { debounce } from "../../helpers/debounce";
import { bytesReadable } from "../../helpers/types";
import SMMessage from "../../components/SMMessage.vue";
import SMFileLink from "../../components/SMFileLink.vue";
import { useUserStore } from "../../store/UserStore";
@@ -161,7 +162,7 @@ const loadFromServer = async () => {
serverItemsLength.value = res.data.total;
} catch (err) {
formMessage.message = parseErrorType(err);
// formMessage.message = parseErrorTyp(err);
}
formLoading.value = false;

View File

@@ -51,14 +51,14 @@
<script setup lang="ts">
import { ref, watch, reactive } from "vue";
import EasyDataTable from "vue3-easy-data-table";
import { relativeDate } from "../../helpers/datetime";
import { SMDate } from "../../helpers/datetime";
import { useRouter } from "vue-router";
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
import { openDialog } from "vue3-promise-dialog";
import { api } from "../../helpers/api";
import SMToolbar from "../../components/SMToolbar.vue";
import SMButton from "../../components/SMButton.vue";
import { debounce } from "../../helpers/common";
import { debounce } from "../../helpers/debounce";
import SMHeading from "../../components/SMHeading.vue";
import SMMessage from "../../components/SMMessage.vue";
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
@@ -124,18 +124,15 @@ const loadFromServer = async () => {
items.value.forEach((row) => {
if (row.created_at !== "undefined") {
row.created_at = relativeDate(
timestampUtcToLocal(row.created_at)
row.created_at = new SMDate(row.created_at, {format: 'yMd', utc: true}).relative();
);
}
if (row.updated_at !== "undefined") {
row.updated_at = relativeDate(
timestampUtcToLocal(row.updated_at)
row.updated_at = new SMDate(row.updated_at, {format: 'yMd', utc: true}).relative();
);
}
if (row.publish_at !== "undefined") {
row.publish_at = relativeDate(
timestampUtcToLocal(row.publish_at)
row.publish_at = new SMDate(row.publish_at, {format: 'yMd', utc: true}).relative();
);
}
});