mirror of
https://github.com/bakhirev/assayo.git
synced 2024-11-16 16:21:41 +00:00
update
This commit is contained in:
parent
b87d79cff9
commit
407d51c291
13
build/asset-manifest.json
Normal file
13
build/asset-manifest.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.1e4b6bfb.css",
|
||||
"main.js": "./static/js/main.a991d9b4.js",
|
||||
"index.html": "./index.html",
|
||||
"main.1e4b6bfb.css.map": "./static/css/main.1e4b6bfb.css.map",
|
||||
"main.a991d9b4.js.map": "./static/js/main.a991d9b4.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.1e4b6bfb.css",
|
||||
"static/js/main.a991d9b4.js"
|
||||
]
|
||||
}
|
|
@ -1 +1 @@
|
|||
<!doctype html><html><head><meta name="viewport" content="width=device-width,height=device-height,initial-scale=1,user-scalable=no,maximum-scale=1"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><meta http-equiv="Cache-Control" content="no-cache"><meta http-equiv="cleartype" content="on"><meta name="HandheldFriendly" content="True"><meta name="format-detection" content="telephone=no"><meta name="format-detection" content="address=no"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="theme-color" content="white"/><meta name="defaultLanguage" content="ru"><meta name="availableLanguages" content="en, es, fr, ja, pt, de, zh, ru"><link rel="canonical" href="https://assayo.online/demo/"><script type="text/javascript">var report=[]</script><script src='./log.txt'></script><script src='./log-1.txt'></script><script src='./log-2.txt'></script><script src='./log-3.txt'></script><script src='./log-4.txt'></script><script src='./log-5.txt'></script><script src='./log-6.txt'></script><script src='../log.txt'></script><script src='../log-1.txt'></script><script src='../log-2.txt'></script><script src='../log-3.txt'></script><script src='../log-4.txt'></script><script src='../log-5.txt'></script><script src='../log-6.txt'></script><script src='../../log.txt'></script><script src='../../log-1.txt'></script><script src='../../log-2.txt'></script><script src='../../log-3.txt'></script><script src='../../log-4.txt'></script><script src='../../log-5.txt'></script><script src='../../log-6.txt'></script><script src='/log.txt'></script><script src='/log-1.txt'></script><script src='/log-2.txt'></script><script src='/log-3.txt'></script><script src='/log-4.txt'></script><script src='/log-5.txt'></script><script src='/log-6.txt'></script><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./logo192.png"/><link rel="manifest" href="./manifest.json"/><title>Git Statistics</title><meta name="description" content="Simple and fast report on git commit history."><meta name="keywords" content="git, statistics, audit, history, log, monitoring, employee control"><meta name="author" content="Bakhirev Aleksei"><meta name="copyright" content="(c) Bakhirev Aleksei"><meta http-equiv="Reply-to" content="alexey-bakhirev@yandex.ru"><meta name="application-name" content="Git statistics"><meta name="msapplication-tooltip" content="Simple and fast report on Git commit history."><meta property="og:title" content="Git Statistics"><meta property="og:description" content="Simple and fast report on Git commit history."><meta property="og:image" content="https://assayo.online/assets/seo/custom_icon_256.png"><meta property="og:site_name" content="Assayo"><meta property="og:url" content="https://assayo.online/"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="Git Statistics"><meta name="twitter:description" content="Simple and fast report on Git commit history."><meta name="twitter:creator" content="Bakhirev Aleksei"><meta name="twitter:image:src" content="https://assayo.online/assets/seo/custom_icon_256.png"><meta name="twitter:domain" content="assayo.online"><meta name="twitter:site" content="assayo.online"><meta itemprop="name" content="Git Statistics"><meta itemprop="description" content="Simple and fast report on Git commit history."><meta itemprop="image" content="https://assayo.online/assets/seo/custom_icon_256.png"><script defer="defer" src="./static/index.js"></script><link href="./static/index.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html><head><meta name="viewport" content="width=device-width,height=device-height,initial-scale=1,user-scalable=no,maximum-scale=1"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><meta http-equiv="Cache-Control" content="no-cache"><meta http-equiv="cleartype" content="on"><meta name="HandheldFriendly" content="True"><meta name="format-detection" content="telephone=no"><meta name="format-detection" content="address=no"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="theme-color" content="white"/><meta name="defaultLanguage" content="ru"><meta name="availableLanguages" content="en, es, fr, ja, pt, de, zh, ru"><link rel="canonical" href="https://assayo.online/demo/"><script type="text/javascript">var report=[]</script><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./logo192.png"/><link rel="manifest" href="./manifest.json"/><title>Git Statistics</title><meta name="description" content="Simple and fast report on git commit history."><meta name="keywords" content="git, statistics, audit, history, log, monitoring, employee control"><meta name="author" content="Bakhirev Aleksei"><meta name="copyright" content="(c) Bakhirev Aleksei"><meta http-equiv="Reply-to" content="alexey-bakhirev@yandex.ru"><meta name="application-name" content="Git statistics"><meta name="msapplication-tooltip" content="Simple and fast report on Git commit history."><meta property="og:title" content="Git Statistics"><meta property="og:description" content="Simple and fast report on Git commit history."><meta property="og:image" content="https://assayo.online/assets/seo/custom_icon_256.png"><meta property="og:site_name" content="Assayo"><meta property="og:url" content="https://assayo.online/"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="Git Statistics"><meta name="twitter:description" content="Simple and fast report on Git commit history."><meta name="twitter:creator" content="Bakhirev Aleksei"><meta name="twitter:image:src" content="https://assayo.online/assets/seo/custom_icon_256.png"><meta name="twitter:domain" content="assayo.online"><meta name="twitter:site" content="assayo.online"><meta itemprop="name" content="Git Statistics"><meta itemprop="description" content="Simple and fast report on Git commit history."><meta itemprop="image" content="https://assayo.online/assets/seo/custom_icon_256.png"><script defer="defer" src="./static/js/main.a991d9b4.js"></script><link href="./static/css/main.1e4b6bfb.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
File diff suppressed because one or more lines are too long
1
build/static/css/main.1e4b6bfb.css.map
Normal file
1
build/static/css/main.1e4b6bfb.css.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
3
build/static/js/main.a991d9b4.js
Normal file
3
build/static/js/main.a991d9b4.js
Normal file
File diff suppressed because one or more lines are too long
76
build/static/js/main.a991d9b4.js.LICENSE.txt
Normal file
76
build/static/js/main.a991d9b4.js.LICENSE.txt
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*! sheetjs (C) 2013-present SheetJS -- http://sheetjs.com */
|
||||
|
||||
/*! xlsx.js (C) 2013-present SheetJS -- http://sheetjs.com */
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react-dom.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react-jsx-runtime.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* scheduler.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @remix-run/router v1.3.1
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* React Router DOM v6.8.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* React Router v6.8.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
1
build/static/js/main.a991d9b4.js.map
Normal file
1
build/static/js/main.a991d9b4.js.map
Normal file
File diff suppressed because one or more lines are too long
78281
build/test.txt
Normal file
78281
build/test.txt
Normal file
File diff suppressed because it is too large
Load diff
79
src/ts/components/CustomSelect/components/List.tsx
Normal file
79
src/ts/components/CustomSelect/components/List.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import UiKitSelectOption from './Option';
|
||||
|
||||
import style from '../styles/index.module.scss';
|
||||
|
||||
interface UiKitSelectListProps {
|
||||
value: any;
|
||||
options: any;
|
||||
search?: string;
|
||||
keyCode?: string;
|
||||
setKeyCode: Function;
|
||||
className?: string;
|
||||
onClick: Function;
|
||||
}
|
||||
|
||||
function UiKitSelectList({
|
||||
value,
|
||||
options,
|
||||
search,
|
||||
keyCode,
|
||||
className,
|
||||
setKeyCode,
|
||||
onClick,
|
||||
}: UiKitSelectListProps) {
|
||||
const [selectedIndex, setSelectedIndex] = useState<number>(-1);
|
||||
|
||||
console.log(value);
|
||||
const searchResult = options
|
||||
?.filter((option: any) => option.title.indexOf(search) !== -1);
|
||||
|
||||
useEffect(() => {
|
||||
if (!keyCode) return;
|
||||
if (keyCode === 'down') {
|
||||
let nextIndex = selectedIndex + 1;
|
||||
if (nextIndex >= searchResult.length) nextIndex = 0;
|
||||
setSelectedIndex(nextIndex);
|
||||
}
|
||||
if (keyCode === 'up') {
|
||||
let nextIndex = selectedIndex - 1;
|
||||
if (nextIndex < 0) nextIndex = searchResult.length - 1;
|
||||
setSelectedIndex(nextIndex);
|
||||
}
|
||||
if (keyCode === 'enter') {
|
||||
const selected = searchResult[selectedIndex];
|
||||
onClick(selected, selectedIndex);
|
||||
}
|
||||
setKeyCode('');
|
||||
}, [keyCode]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIndex(-1);
|
||||
}, [search]);
|
||||
|
||||
const items = searchResult?.map((option: any, index: number) => {
|
||||
return (
|
||||
<UiKitSelectOption
|
||||
key={option.id}
|
||||
focus={index === selectedIndex}
|
||||
option={option}
|
||||
className={className}
|
||||
onClick={() => {
|
||||
onClick(option.source, index);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
if (!items.length) return null;
|
||||
|
||||
return (
|
||||
<ul className={`${style.ui_kit_select_list} scroll_y ${className || ''}`}>
|
||||
{items}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default UiKitSelectList;
|
33
src/ts/components/CustomSelect/components/Option.tsx
Normal file
33
src/ts/components/CustomSelect/components/Option.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
|
||||
import style from '../styles/index.module.scss';
|
||||
|
||||
interface UiKitSelectOptionProps {
|
||||
option: any;
|
||||
focus?: boolean;
|
||||
className?: string;
|
||||
onClick: (event: React.MouseEvent<HTMLLIElement, MouseEvent>) => void;
|
||||
}
|
||||
|
||||
function UiKitSelectOption({
|
||||
option,
|
||||
focus,
|
||||
className,
|
||||
onClick,
|
||||
}: UiKitSelectOptionProps) {
|
||||
let localClassName = [style.ui_kit_select_option];
|
||||
if (className) localClassName.push(className);
|
||||
if (focus) localClassName.push(style.ui_kit_select_option_focus);
|
||||
|
||||
return (
|
||||
<li
|
||||
className={localClassName.join(' ')}
|
||||
onClick={onClick}
|
||||
>
|
||||
{option.title}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default UiKitSelectOption;
|
74
src/ts/components/CustomSelect/components/Search.tsx
Normal file
74
src/ts/components/CustomSelect/components/Search.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { CLOSE_DELAY } from '../helpers/constants';
|
||||
import style from '../styles/index.module.scss';
|
||||
|
||||
function getKeyCode(key?: string): string {
|
||||
return {
|
||||
ArrowUp: 'up',
|
||||
ArrowDown: 'down',
|
||||
Enter: 'enter',
|
||||
}[key || ''] || '';
|
||||
}
|
||||
|
||||
interface UiKitSelectSearchProps {
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
onClose: Function;
|
||||
onChange: Function;
|
||||
onKeyDown: Function;
|
||||
}
|
||||
|
||||
function UiKitSelectSearch({
|
||||
value,
|
||||
placeholder,
|
||||
className,
|
||||
onChange,
|
||||
onClose,
|
||||
onKeyDown,
|
||||
}: UiKitSelectSearchProps) {
|
||||
const ref = useRef(null);
|
||||
const [timer, setTimer] = useState<any>(0);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
// @ts-ignore
|
||||
if (ref?.current) ref.current.focus();
|
||||
}, CLOSE_DELAY);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
className={`${style.ui_kit_select_search} ${className || ''}`}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
clearTimeout(timer);
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
clearTimeout(timer);
|
||||
const timerId = setTimeout(() => {
|
||||
onClose();
|
||||
}, CLOSE_DELAY);
|
||||
setTimer(timerId);
|
||||
}}
|
||||
onFocus={() => {
|
||||
clearTimeout(timer);
|
||||
}}
|
||||
onKeyDown={() => {
|
||||
// onKeyDown(getKeyCode(event.key));
|
||||
}}
|
||||
onKeyUp={(event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
onKeyDown(getKeyCode(event.key));
|
||||
// onKeyDown('');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default UiKitSelectSearch;
|
28
src/ts/components/CustomSelect/components/Value.tsx
Normal file
28
src/ts/components/CustomSelect/components/Value.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import React from 'react';
|
||||
|
||||
import style from '../styles/index.module.scss';
|
||||
|
||||
interface UiKitSelectValueProps {
|
||||
value: any;
|
||||
options: any;
|
||||
className?: string;
|
||||
onClick: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
}
|
||||
|
||||
function UiKitSelectValue({
|
||||
value,
|
||||
className,
|
||||
onClick,
|
||||
}: UiKitSelectValueProps) {
|
||||
return (
|
||||
<div
|
||||
className={`${style.ui_kit_select_value} ${className || ''}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default UiKitSelectValue;
|
1
src/ts/components/CustomSelect/helpers/constants.ts
Normal file
1
src/ts/components/CustomSelect/helpers/constants.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const CLOSE_DELAY = 250;
|
49
src/ts/components/CustomSelect/helpers/index.ts
Normal file
49
src/ts/components/CustomSelect/helpers/index.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
function getStringFromObject(value: any) {
|
||||
return value?.title
|
||||
|| value?.name
|
||||
|| value?.label
|
||||
|| value?.description
|
||||
|| value?.value
|
||||
|| value?.id
|
||||
|| value?.uuid
|
||||
|| value?.key
|
||||
|| JSON.stringify(value);
|
||||
}
|
||||
|
||||
function getIdFromObject(value: any, index: number) {
|
||||
return value?.id
|
||||
?? value?.uuid
|
||||
?? value?.key
|
||||
?? index
|
||||
?? getStringFromObject(value);
|
||||
}
|
||||
|
||||
function getValue(
|
||||
value: any,
|
||||
formatter: (a: any, i?: number) => string,
|
||||
) {
|
||||
const type = typeof value;
|
||||
if (type === 'boolean') return value ? 'yes' : 'no';
|
||||
if (type === 'number' || type === 'string') return value;
|
||||
if (!value) return '';
|
||||
|
||||
return Array.isArray(value)
|
||||
? value.map(formatter).join(', ')
|
||||
: formatter(value);
|
||||
}
|
||||
|
||||
export function getTitle(value: any) {
|
||||
return getValue(value, getStringFromObject);
|
||||
}
|
||||
|
||||
export function getId(value: any, index: number) {
|
||||
return getValue(value, (v: any) => getIdFromObject(v, index));
|
||||
}
|
||||
|
||||
export function getOption(value: any, index: number) {
|
||||
return {
|
||||
id: getId(value, index),
|
||||
title: getTitle(value),
|
||||
source: value,
|
||||
};
|
||||
}
|
78
src/ts/components/CustomSelect/index.tsx
Normal file
78
src/ts/components/CustomSelect/index.tsx
Normal file
|
@ -0,0 +1,78 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import UiKitSelectValue from './components/Value';
|
||||
import UiKitSelectSearch from './components/Search';
|
||||
import UiKitSelectList from './components/List';
|
||||
import { getOption, getTitle } from './helpers';
|
||||
|
||||
import style from './styles/index.module.scss';
|
||||
|
||||
interface UiKitSelectProps {
|
||||
value: any;
|
||||
options: any;
|
||||
className?: string;
|
||||
onChange?: Function;
|
||||
}
|
||||
|
||||
function UiKitSelect({
|
||||
value,
|
||||
options,
|
||||
className,
|
||||
onChange,
|
||||
}: UiKitSelectProps) {
|
||||
const [openSearch, setOpenSearch] = useState<boolean>(false);
|
||||
const [search, setSearch] = useState<string>('');
|
||||
const [keyCode, setKeyCode] = useState<string>('');
|
||||
|
||||
const formattedOptions = useMemo(() => options?.map(getOption) || [], [options]);
|
||||
const formattedValue = useMemo(() => {
|
||||
const selectedOption = options.find((option: any) => option.id === value);
|
||||
return getTitle(selectedOption) || getTitle(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className={`${style.ui_kit_select_container} ${className || ''}`}>
|
||||
{!openSearch ? (
|
||||
<UiKitSelectValue
|
||||
value={formattedValue}
|
||||
options={formattedOptions}
|
||||
className={className}
|
||||
onClick={() => setOpenSearch(true)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{openSearch ? (
|
||||
<UiKitSelectSearch
|
||||
value={search}
|
||||
placeholder={formattedValue}
|
||||
className={className}
|
||||
onChange={setSearch}
|
||||
onKeyDown={setKeyCode}
|
||||
onClose={() => {
|
||||
setSearch('');
|
||||
setOpenSearch(false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{openSearch ? (
|
||||
<UiKitSelectList
|
||||
value={value}
|
||||
options={formattedOptions}
|
||||
search={search}
|
||||
keyCode={keyCode}
|
||||
setKeyCode={setKeyCode}
|
||||
className={className}
|
||||
onClick={(selected: any) => {
|
||||
setSearch('');
|
||||
setOpenSearch(false);
|
||||
if (onChange) onChange(selected?.id);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default UiKitSelect;
|
67
src/ts/components/CustomSelect/styles/index.module.scss
Normal file
67
src/ts/components/CustomSelect/styles/index.module.scss
Normal file
|
@ -0,0 +1,67 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.ui_kit_select {
|
||||
&_value,
|
||||
&_search,
|
||||
&_option {
|
||||
font-size: var(--font-s);
|
||||
font-weight: 100;
|
||||
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
padding: 0 var(--space-l);
|
||||
margin: 0;
|
||||
|
||||
line-height: 42px;
|
||||
text-align: left;
|
||||
box-sizing: border-box;
|
||||
vertical-align: top;
|
||||
outline-color: transparent;
|
||||
|
||||
border-radius: var(--border-radius-s);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-black);
|
||||
background-color: var(--color-white);
|
||||
}
|
||||
|
||||
&_container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&_list {
|
||||
position: absolute;
|
||||
top: 45px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
|
||||
display: block;
|
||||
max-height: 300px;
|
||||
padding: var(--space-xxs) 0;
|
||||
text-align: left;
|
||||
|
||||
border-radius: var(--border-radius-s);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: 0 0 5px var(--color-border);
|
||||
background-color: var(--color-white);
|
||||
}
|
||||
|
||||
&_option {
|
||||
display: block;
|
||||
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
|
||||
&_focus,
|
||||
&:hover {
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui_kit_select_option + .ui_kit_select_option {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
69
src/ts/components/DataView/store/index.ts
Normal file
69
src/ts/components/DataView/store/index.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
import { observable, action, makeObservable } from 'mobx';
|
||||
|
||||
class DataViewStore {
|
||||
key: string = 'view_settings';
|
||||
|
||||
version: number = 1;
|
||||
|
||||
settings: any = {};
|
||||
|
||||
constructor() {
|
||||
this.load();
|
||||
makeObservable(this, {
|
||||
settings: observable,
|
||||
load: action,
|
||||
setItem: action,
|
||||
});
|
||||
}
|
||||
|
||||
load() {
|
||||
const settings = JSON.parse(localStorage.getItem(this.key) || '{}') || {};
|
||||
if (settings.version === this.version) {
|
||||
this.settings = settings.settings;
|
||||
}
|
||||
}
|
||||
|
||||
save() {
|
||||
if (Object.keys(this.settings).length === 0) {
|
||||
localStorage.removeItem(this.key);
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(this.key, JSON.stringify({
|
||||
version: this.version,
|
||||
settings: this.settings,
|
||||
}));
|
||||
}
|
||||
|
||||
#getPath(path: any): string {
|
||||
if (!path) return '';
|
||||
if (Array.isArray(path)) {
|
||||
return path.join('.');
|
||||
} else if (typeof path === 'object') {
|
||||
return [path.type, path.page].join('.');
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
setItem(path: any, value: any, defaultValue?: any) {
|
||||
const formattedPath = this.#getPath(path);
|
||||
if (!formattedPath) return;
|
||||
|
||||
if (!value || value === defaultValue) {
|
||||
delete this.settings[formattedPath];
|
||||
} else {
|
||||
this.settings[formattedPath] = value;
|
||||
}
|
||||
|
||||
this.save();
|
||||
}
|
||||
|
||||
getItem(path: any, defaultValue?: any): any {
|
||||
const formattedPath = this.#getPath(path);
|
||||
return this.settings?.[formattedPath] || defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
const dataViewStore = new DataViewStore();
|
||||
|
||||
export default dataViewStore;
|
52
src/ts/components/UiKit/components/Checkbox.tsx
Normal file
52
src/ts/components/UiKit/components/Checkbox.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Wrapper, { IUiKitWrapperProps } from './Wrapper';
|
||||
import style from '../styles/checkbox.module.scss';
|
||||
|
||||
interface IUiKitCheckboxProps extends IUiKitWrapperProps {
|
||||
value: any;
|
||||
onChange: Function;
|
||||
}
|
||||
|
||||
function UiKitCheckbox({
|
||||
title,
|
||||
description,
|
||||
help,
|
||||
error,
|
||||
className,
|
||||
|
||||
value,
|
||||
onChange,
|
||||
}: IUiKitCheckboxProps) {
|
||||
const { t } = useTranslation();
|
||||
const id = `checkbox-${Math.ceil(Math.random() * 10000)}`;
|
||||
return (
|
||||
<Wrapper
|
||||
description={description}
|
||||
help={help}
|
||||
error={error}
|
||||
className={className}
|
||||
>
|
||||
<div className={style.ui_kit_checkbox}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={id}
|
||||
checked={!!value}
|
||||
className={style.ui_kit_checkbox_box}
|
||||
onChange={() => {
|
||||
onChange(!value);
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
htmlFor={id}
|
||||
className={style.ui_kit_checkbox_title}
|
||||
>
|
||||
{t(title || '')}
|
||||
</label>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default UiKitCheckbox;
|
35
src/ts/components/UiKit/styles/checkbox.module.scss
Normal file
35
src/ts/components/UiKit/styles/checkbox.module.scss
Normal file
|
@ -0,0 +1,35 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.ui_kit_checkbox {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 0 0 0 32px;
|
||||
|
||||
&_box {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: block;
|
||||
width: var(--space-xl);
|
||||
height: var(--space-xl);
|
||||
|
||||
cursor: pointer;
|
||||
box-shadow: none;
|
||||
border-radius: var(--border-radius-s);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
&_title {
|
||||
font-size: var(--font-s);
|
||||
font-weight: 100;
|
||||
|
||||
display: block;
|
||||
padding: var(--space-xxxs) 0 0;
|
||||
|
||||
cursor: pointer;
|
||||
white-space: normal;
|
||||
|
||||
color: var(--color-black);
|
||||
}
|
||||
}
|
21
src/ts/helpers/Depersonalized/FakeName.ts
Normal file
21
src/ts/helpers/Depersonalized/FakeName.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import IHashMap from 'ts/interfaces/HashMap';
|
||||
|
||||
export default class FakeName {
|
||||
refOldNewName: IHashMap<string> = {};
|
||||
|
||||
dictionary: string[] = [];
|
||||
|
||||
index: number = 0;
|
||||
|
||||
constructor(dictionary: string[]) {
|
||||
this.dictionary = dictionary;
|
||||
}
|
||||
|
||||
get(name: string) {
|
||||
if (!this.refOldNewName[name]) {
|
||||
this.refOldNewName[name] = this.dictionary[this.index] || `${Math.random()}`;
|
||||
this.index += 1;
|
||||
}
|
||||
return this.refOldNewName[name];
|
||||
}
|
||||
}
|
111
src/ts/helpers/Depersonalized/constants.ts
Normal file
111
src/ts/helpers/Depersonalized/constants.ts
Normal file
|
@ -0,0 +1,111 @@
|
|||
export const FAKE_AUTHORS = [
|
||||
'Fyodor Dostoevsky',
|
||||
'Dante Alighieri',
|
||||
'Lev Tolstoy',
|
||||
'Victor Hugo',
|
||||
'William Shakespeare',
|
||||
'Johann Wolfgang von Goethe',
|
||||
'Miguel de Cervantes y Saavedra',
|
||||
'Italo Calvino',
|
||||
'Stendhal',
|
||||
'Charles Baudelaire',
|
||||
'Marcel Proust',
|
||||
'Giovanni Boccaccio',
|
||||
'Alexander Pushkin',
|
||||
'Jalaluddin Muhammad Rumi',
|
||||
'Franz Kafka',
|
||||
'Anton Chekhov',
|
||||
'Gabriel García Márquez',
|
||||
'Umberto Eco',
|
||||
'J.R.R. Tolkien',
|
||||
'William Faulkner',
|
||||
'Aesop',
|
||||
'Arthur Rimbaud',
|
||||
'Aristophanes',
|
||||
'Ivan Turgenev',
|
||||
'Sophocles',
|
||||
'Molière',
|
||||
'Charles Dickens',
|
||||
'Maxim Gorky',
|
||||
'George Orwell',
|
||||
'Edgar Allan Poe',
|
||||
'Publius Vergilius Maro',
|
||||
'Julio Cortázar',
|
||||
'Nazim Hikmet',
|
||||
'Oscar Wilde',
|
||||
'Jean de La Fontaine',
|
||||
'Rainer Maria Rilke',
|
||||
'Lord Byron',
|
||||
'Hans Christian Andersen',
|
||||
'Thomas Mann',
|
||||
'Alexandre Dumas',
|
||||
'James Joyce',
|
||||
'Louis-Ferdinand Céline',
|
||||
'Boris Pasternak',
|
||||
'Federico García Lorca',
|
||||
'Pablo Neruda',
|
||||
'Borges',
|
||||
'Beaumarchais',
|
||||
'Naguib Mahfouz',
|
||||
'Ursula K. Le Guin',
|
||||
'Nikolay Gogol',
|
||||
'Honoré de Balzac',
|
||||
'Ernest Hemingway',
|
||||
'Neil Gaiman',
|
||||
'Jean Racine',
|
||||
'Albert Camus',
|
||||
'Jean-Paul Sartre',
|
||||
'Chingiz Aitmatov',
|
||||
'John Steinbeck',
|
||||
'Milan Kundera',
|
||||
'Jules Verne',
|
||||
'Mark Twain',
|
||||
'Francois Rabelais',
|
||||
'Yasar Kemal',
|
||||
'George Bernard Shaw',
|
||||
'Arthur Conan Doyle',
|
||||
'Jane Austen',
|
||||
'Geoffrey Chaucer',
|
||||
'Antoine de Saint-Exupéry',
|
||||
'Erich Maria Remarque',
|
||||
'J.D. Salinger',
|
||||
'Virginia Woolf',
|
||||
'Louis Aragon',
|
||||
'Herman Melville',
|
||||
'Alphonse Daudet',
|
||||
'Mikhail Sholokhov',
|
||||
'Stefan Zweig',
|
||||
'José Saramago',
|
||||
'Bertolt Brecht',
|
||||
'Mario Vargas Llosa',
|
||||
'T.S. Eliot',
|
||||
'Guy de Maupassant',
|
||||
'John Keats',
|
||||
'Sabahattin Ali',
|
||||
'Ahmet Hamdi Tanpinar',
|
||||
'John Fante',
|
||||
'Henri-Frédéric Blanc',
|
||||
'Isaac Asimov',
|
||||
'Fitzgerald Scott',
|
||||
'J.M. Coetzee',
|
||||
'Kazuo Ishiguro',
|
||||
'Hermann Hesse',
|
||||
'Robert Louis Stevenson',
|
||||
'Salman Rushdie',
|
||||
'Mario Vargas Llosa',
|
||||
'Aldous Huxley',
|
||||
'Paul Valéry',
|
||||
'Thomas Pynchon',
|
||||
'H.P. Lovecraft',
|
||||
'Haruki Murakami',
|
||||
'Nikos Kazantzakis',
|
||||
];
|
||||
|
||||
export const FAKE_EMAILS = FAKE_AUTHORS
|
||||
.map((name: string, index: number) => (
|
||||
name.replace(/([\s.]+)|([^A-Za-z]+)/gim, '-').toLowerCase() + index + '@yahoo.com'
|
||||
));
|
||||
|
||||
export const FAKE_TASK_PREFIXES = 'axeurtyqwpsdfghjklzcvbnm'
|
||||
.split('')
|
||||
.map((symbol) => (new Array(5)).fill(symbol.toUpperCase()).join(''));
|
60
src/ts/helpers/Depersonalized/index.ts
Normal file
60
src/ts/helpers/Depersonalized/index.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import ICommit, { ISystemCommit } from 'ts/interfaces/Commit';
|
||||
|
||||
import {
|
||||
FAKE_AUTHORS,
|
||||
FAKE_EMAILS,
|
||||
FAKE_TASK_PREFIXES,
|
||||
} from './constants';
|
||||
import FakeName from './FakeName';
|
||||
|
||||
export default class Depersonalized {
|
||||
fakeName: any = null;
|
||||
|
||||
fakeEmail: any = null;
|
||||
|
||||
fakeTaskPrefix: any = null;
|
||||
|
||||
constructor() {
|
||||
this.fakeName = new FakeName(FAKE_AUTHORS);
|
||||
this.fakeEmail = new FakeName(FAKE_EMAILS);
|
||||
this.fakeTaskPrefix = new FakeName(FAKE_TASK_PREFIXES);
|
||||
}
|
||||
|
||||
getCommit(commit: ICommit | ISystemCommit): ICommit | ISystemCommit {
|
||||
const author = this.fakeName.get(commit.author);
|
||||
const email = this.fakeEmail.get(commit.author);
|
||||
|
||||
if (!commit.task) {
|
||||
return {
|
||||
...commit,
|
||||
author,
|
||||
email,
|
||||
};
|
||||
}
|
||||
|
||||
const taskPrefix = commit.task.split(/[-_\s:#=]+/gim).shift() || '';
|
||||
const newTaskPrefix = this.fakeTaskPrefix.get(taskPrefix);
|
||||
const task = commit.task.replace(taskPrefix, newTaskPrefix);
|
||||
const message = commit.message.replace(taskPrefix, newTaskPrefix);
|
||||
|
||||
// @ts-ignore
|
||||
const branch = commit.branch // @ts-ignore
|
||||
? commit.branch.replace(taskPrefix, newTaskPrefix)
|
||||
: undefined;
|
||||
|
||||
// @ts-ignore
|
||||
const toBranch = commit.toBranch// @ts-ignore
|
||||
? commit.toBranch.replace(taskPrefix, newTaskPrefix)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...commit,
|
||||
task,
|
||||
message,
|
||||
author,
|
||||
email,
|
||||
branch,
|
||||
toBranch,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -3,9 +3,11 @@ import { observer } from 'mobx-react-lite';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import InputString from 'ts/components/UiKit/components/InputString';
|
||||
import Select from 'ts/components/UiKit/components/Select';
|
||||
import UiKitSelect from 'ts/components/UiKit/components/Select';
|
||||
import UiKitCheckbox from 'ts/components/UiKit/components/Checkbox';
|
||||
import PageBox from 'ts/components/Page/Box';
|
||||
import Title from 'ts/components/Title';
|
||||
import dataGripStore from 'ts/store/DataGrip';
|
||||
import localization from 'ts/helpers/Localization';
|
||||
import { applicationHasCustom } from 'ts/helpers/RPC';
|
||||
|
||||
|
@ -32,7 +34,7 @@ const Common = observer((): React.ReactElement | null => {
|
|||
applicationHasCustom.title = true;
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
<UiKitSelect
|
||||
title="page.settings.document.language"
|
||||
value={language}
|
||||
options={[
|
||||
|
@ -50,6 +52,13 @@ const Common = observer((): React.ReactElement | null => {
|
|||
setLanguage(id);
|
||||
}}
|
||||
/>
|
||||
<UiKitCheckbox
|
||||
value={dataGripStore.isDepersonalized}
|
||||
title="page.settings.document.depersonalize"
|
||||
onChange={() => {
|
||||
dataGripStore.depersonalized(!dataGripStore.isDepersonalized);
|
||||
}}
|
||||
/>
|
||||
</PageBox>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -10,6 +10,7 @@ import getTitle from 'ts/helpers/Title';
|
|||
|
||||
import { setDefaultValues } from 'ts/pages/Settings/helpers/getEmptySettings';
|
||||
import { applicationHasCustom } from 'ts/helpers/RPC';
|
||||
import Depersonalized from 'ts/helpers/Depersonalized';
|
||||
|
||||
import filtersInHeaderStore from './FiltersInHeader';
|
||||
|
||||
|
@ -24,6 +25,7 @@ interface IDataGripStore {
|
|||
dataGrip: any;
|
||||
fileGrip: any;
|
||||
status: DataParseStatusEnum;
|
||||
isDepersonalized: boolean;
|
||||
setCommits: (log?: string[]) => void;
|
||||
}
|
||||
|
||||
|
@ -36,6 +38,8 @@ class DataGripStore implements IDataGripStore {
|
|||
|
||||
hash: number = 0;
|
||||
|
||||
isDepersonalized: boolean = false;
|
||||
|
||||
status: DataParseStatusEnum = DataParseStatusEnum.PROCESSING;
|
||||
|
||||
constructor() {
|
||||
|
@ -43,8 +47,10 @@ class DataGripStore implements IDataGripStore {
|
|||
commits: observable,
|
||||
dataGrip: observable,
|
||||
hash: observable,
|
||||
isDepersonalized: observable,
|
||||
status: observable,
|
||||
setCommits: action,
|
||||
depersonalized: action,
|
||||
updateStatistic: action,
|
||||
});
|
||||
}
|
||||
|
@ -88,15 +94,31 @@ class DataGripStore implements IDataGripStore {
|
|||
}
|
||||
}
|
||||
|
||||
depersonalized(status?: boolean) {
|
||||
this.isDepersonalized = !!status;
|
||||
setTimeout(() => {
|
||||
this.updateStatistic();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
updateStatistic() {
|
||||
dataGrip.clear();
|
||||
fileGrip.clear();
|
||||
|
||||
const depersonalized = new Depersonalized();
|
||||
this.commits.forEach((commit: ICommit | ISystemCommit) => {
|
||||
if (commit.timestamp < filtersInHeaderStore.from
|
||||
|| commit.timestamp > filtersInHeaderStore.to) return;
|
||||
dataGrip.addCommit(commit);
|
||||
fileGrip.addCommit(commit);
|
||||
|
||||
const localCommit = this.isDepersonalized
|
||||
? depersonalized.getCommit(commit)
|
||||
: commit;
|
||||
|
||||
dataGrip.addCommit(localCommit);
|
||||
fileGrip.addCommit(localCommit);
|
||||
});
|
||||
|
||||
console.log(depersonalized.fakeTaskPrefix);
|
||||
fileGrip.updateTotalInfo();
|
||||
dataGrip.updateTotalInfo();
|
||||
achievements.updateByGrip(dataGrip, fileGrip);
|
||||
|
|
71
src/ts/store/FiltersInHeader.ts
Normal file
71
src/ts/store/FiltersInHeader.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { makeObservable, observable, action } from 'mobx';
|
||||
import { ONE_DAY } from 'ts/helpers/formatter';
|
||||
|
||||
import ICommit from '../interfaces/Commit';
|
||||
|
||||
interface IFiltersInHeaderStore {
|
||||
defaultFrom: string;
|
||||
defaultTo: string;
|
||||
from: string;
|
||||
to: string;
|
||||
|
||||
updateByCommits: (firstCommit: ICommit, lastCommit: ICommit) => void,
|
||||
setFilterByDateType: (type: string) => void,
|
||||
}
|
||||
|
||||
class FiltersInHeaderStore implements IFiltersInHeaderStore {
|
||||
defaultFrom: string = ''; // "2021-02-09"
|
||||
|
||||
defaultTo: string = ''; // "2021-02-09"
|
||||
|
||||
from: string = ''; // "2021-02-09"
|
||||
|
||||
to: string = ''; // "2021-02-09"
|
||||
|
||||
lastCommitTime: number = 0; // 1612828800000
|
||||
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
defaultFrom: observable,
|
||||
defaultTo: observable,
|
||||
from: observable,
|
||||
to: observable,
|
||||
|
||||
updateByCommits: action,
|
||||
setFilterByDateType: action,
|
||||
updateProperty: action,
|
||||
});
|
||||
}
|
||||
|
||||
updateByCommits(firstCommit: ICommit, lastCommit: ICommit) {
|
||||
this.defaultFrom = firstCommit.timestamp;
|
||||
this.defaultTo = lastCommit.timestamp;
|
||||
this.from = this.defaultFrom;
|
||||
this.to = this.defaultTo;
|
||||
this.lastCommitTime = (new Date(this.defaultTo)).getTime();
|
||||
}
|
||||
|
||||
setFilterByDateType(type: string) {
|
||||
const count = {
|
||||
year: 365,
|
||||
halfYear: 183,
|
||||
month: 30,
|
||||
week: 7,
|
||||
day: 1,
|
||||
}[type];
|
||||
|
||||
this.from = count
|
||||
? (new Date(this.lastCommitTime - ONE_DAY * count)).toISOString().split('T')[0]
|
||||
: this.defaultFrom;
|
||||
|
||||
this.to = this.defaultTo;
|
||||
}
|
||||
|
||||
updateProperty(propertyName: string, value?: any) {
|
||||
this[propertyName] = value ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
const filtersInHeaderStore = new FiltersInHeaderStore();
|
||||
|
||||
export default filtersInHeaderStore;
|
|
@ -2,6 +2,7 @@ export default `
|
|||
§ page.settings.document.title: Display settings
|
||||
§ page.settings.document.name: Page title
|
||||
§ page.settings.document.language: Interface language
|
||||
§ page.settings.document.depersonalize: Depersonalize the data
|
||||
§ page.settings.links.title: Link prefixes
|
||||
§ page.settings.links.task: For task numbers
|
||||
§ page.settings.links.pr: For PR
|
||||
|
|
|
@ -2,6 +2,7 @@ export default `
|
|||
§ page.settings.document.title: Display settings
|
||||
§ page.settings.document.name: Page title
|
||||
§ page.settings.document.language: Interface language
|
||||
§ page.settings.document.depersonalize: Depersonalize the data
|
||||
§ page.settings.links.title: Link prefixes
|
||||
§ page.settings.links.task: For task numbers
|
||||
§ page.settings.links.pr: For PR
|
||||
|
|
|
@ -2,6 +2,7 @@ export default `
|
|||
§ page.settings.document.title: Настройки отображения
|
||||
§ page.settings.document.name: Заголовок страницы
|
||||
§ page.settings.document.language: Язык интерфейса
|
||||
§ page.settings.document.depersonalize: Depersonalize the data
|
||||
§ page.settings.links.title: Префиксы ссылок
|
||||
§ page.settings.links.task: Для номеров задач
|
||||
§ page.settings.links.pr: Для PR
|
||||
|
@ -24,4 +25,4 @@ export default `
|
|||
§ page.settings.form.remove: Удалить
|
||||
§ page.settings.form.addEmployee: Добавить сотрудника
|
||||
§ page.settings.form.addContract: Добавить трудовой договор
|
||||
`;
|
||||
`;
|
||||
|
|
|
@ -2,6 +2,7 @@ export default `
|
|||
§ page.settings.document.title: Display settings
|
||||
§ page.settings.document.name: Page title
|
||||
§ page.settings.document.language: Interface language
|
||||
§ page.settings.document.depersonalize: Depersonalize the data
|
||||
§ page.settings.links.title: Link prefixes
|
||||
§ page.settings.links.task: For task numbers
|
||||
§ page.settings.links.pr: For PR
|
||||
|
|
|
@ -2,6 +2,7 @@ export default `
|
|||
§ page.settings.document.title: Display settings
|
||||
§ page.settings.document.name: Page title
|
||||
§ page.settings.document.language: Interface language
|
||||
§ page.settings.document.depersonalize: Depersonalize the data
|
||||
§ page.settings.links.title: Link prefixes
|
||||
§ page.settings.links.task: For task numbers
|
||||
§ page.settings.links.pr: For PR
|
||||
|
|
|
@ -2,6 +2,7 @@ export default `
|
|||
§ page.settings.document.title: Display settings
|
||||
§ page.settings.document.name: Page title
|
||||
§ page.settings.document.language: Interface language
|
||||
§ page.settings.document.depersonalize: Depersonalize the data
|
||||
§ page.settings.links.title: Link prefixes
|
||||
§ page.settings.links.task: For task numbers
|
||||
§ page.settings.links.pr: For PR
|
||||
|
|
|
@ -2,6 +2,7 @@ export default `
|
|||
§ page.settings.document.title: Настройки отображения
|
||||
§ page.settings.document.name: Заголовок страницы
|
||||
§ page.settings.document.language: Язык интерфейса
|
||||
§ page.settings.document.depersonalize: Деперсонализировать данные
|
||||
§ page.settings.links.title: Префиксы ссылок
|
||||
§ page.settings.links.task: Для номеров задач
|
||||
§ page.settings.links.pr: Для PR
|
||||
|
|
|
@ -2,6 +2,7 @@ export default `
|
|||
§ page.settings.document.title: Display settings
|
||||
§ page.settings.document.name: Page title
|
||||
§ page.settings.document.language: Interface language
|
||||
§ page.settings.document.depersonalize: Depersonalize the data
|
||||
§ page.settings.links.title: Link prefixes
|
||||
§ page.settings.links.task: For task numbers
|
||||
§ page.settings.links.pr: For PR
|
||||
|
|
Loading…
Reference in a new issue