Compare commits

..

1 Commits

Author SHA1 Message Date
550f0eca06 Started electrobun migration.
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit
2026-03-01 02:56:25 +00:00
375 changed files with 12847 additions and 21628 deletions

2
.gitignore vendored
View File

@ -28,5 +28,3 @@ yarn-error.log*
/design_files/*.af~lock~
stats.html
test-results.xml

28
Jenkinsfile vendored
View File

@ -20,7 +20,7 @@ def deploy() {
stage('Build (Ubuntu)') {
nodejs(nodeJSInstallationName: 'Node23') {
sh "VITE_BUILD_NUMBER=${env.BUILD_NUMBER} NODE_ENV=production pnpm build:cloudflare"
sh 'NODE_ENV=production pnpm build:cloudflare'
}
}
@ -70,41 +70,21 @@ def buildOnLabel(label, buildCommand) {
stage("Build (${label})") {
nodejs(nodeJSInstallationName: 'Node23') {
if (isUnix()) {
sh "VITE_BUILD_NUMBER=${env.BUILD_NUMBER} NODE_ENV=production ${buildCommand}"
sh "NODE_ENV=production ${buildCommand}"
} else {
bat "set VITE_BUILD_NUMBER=${env.BUILD_NUMBER} && set NODE_ENV=production && ${buildCommand}"
bat "set NODE_ENV=production && ${buildCommand}"
}
}
}
stage("Archive Artifacts (${label})") {
archiveArtifacts artifacts: 'app_dist/**/farmcontrol-*.dmg, app_dist/**/farmcontrol-*.exe, app_dist/**/farmcontrol-*.pkg, app_dist/**/farmcontrol-*.msi', fingerprint: true
archiveArtifacts artifacts: 'app_dist/**/*.dmg, app_dist/**/*.exe', fingerprint: true
}
}
}
}
def setBuildNameFromPackageVersion() {
node('ubuntu') {
stage('Set Build Name') {
checkout scm
def version
nodejs(nodeJSInstallationName: 'Node23') {
version = sh(
script: "node -p \"require('./package.json').version\"",
returnStdout: true
).trim()
}
def buildName = "v${version}-b${env.BUILD_NUMBER}"
currentBuild.displayName = buildName
echo "Build name set to: ${buildName}"
}
}
}
try {
setBuildNameFromPackageVersion()
parallel(
'Windows Build': buildOnLabel('windows', 'pnpm build:electron'),
'MacOS Build': buildOnLabel('macos', 'pnpm build:electron'),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.913777,0,0,0.913777,-11.957695,-8.458487)">
<path d="M56.866,48.187C56.775,48.817 56.734,49.463 56.734,50.118C56.734,52.243 57.143,54.279 57.906,56.141C54.515,53.972 49.815,52.352 44,52.352C31.125,52.352 23.734,60.274 23.734,64.477C23.734,64.93 23.953,65.165 24.547,65.165L63.031,65.165L63.031,71.774L24.766,71.774C19.047,71.774 16.219,69.852 16.219,65.774C16.219,56.587 27.547,45.727 44,45.727C48.778,45.727 53.123,46.643 56.866,48.187ZM57.984,27.368C57.984,35.743 51.766,42.477 44,42.477C36.25,42.477 30.031,35.758 30.031,27.399C30.031,19.227 36.328,12.54 44,12.54C51.719,12.54 57.984,19.165 57.984,27.368ZM36.812,27.384C36.812,32.352 40.109,35.868 44,35.868C47.938,35.868 51.203,32.321 51.203,27.368C51.203,22.587 47.922,19.149 44,19.149C40.125,19.149 36.812,22.634 36.812,27.384Z" style="fill-rule:nonzero;"/>
<path d="M72.469,39.508C66.531,39.508 61.812,44.29 61.812,50.118C61.812,54.462 64.266,58.212 68.109,59.899L68.109,74.743C68.109,75.29 68.359,75.821 68.766,76.243L71.609,78.915C72.141,79.399 72.969,79.462 73.562,78.868L78.5,73.946C79.125,73.305 79.062,72.383 78.484,71.774L76.031,69.258L79.641,65.649C80.234,65.055 80.25,64.118 79.609,63.43L76.234,60.087C80.641,57.962 83.125,54.399 83.125,50.118C83.125,44.29 78.375,39.508 72.469,39.508ZM72.453,43.696C74.094,43.696 75.422,45.055 75.422,46.696C75.422,48.321 74.094,49.696 72.453,49.696C70.844,49.696 69.469,48.321 69.469,46.696C69.469,45.055 70.797,43.696 72.453,43.696Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.865066,0,0,0.865066,3,7.501025)">
<path d="M3.656,56.641L63.359,56.641C65.406,56.641 67.047,54.906 67.047,52.891C67.047,50.813 65.422,49.125 63.359,49.125L3.656,49.125C1.609,49.125 0,50.859 0,52.891C0,54.875 1.625,56.641 3.656,56.641Z" style="fill-rule:nonzero;"/>
<path d="M3.656,40.297L63.359,40.297C65.406,40.297 67.047,38.563 67.047,36.547C67.047,34.453 65.422,32.781 63.359,32.781L3.656,32.781C1.609,32.781 0,34.484 0,36.547C0,38.516 1.625,40.297 3.656,40.297Z" style="fill-rule:nonzero;"/>
<path d="M46.469,23.891L63.359,23.938C65.406,23.953 67.047,22.188 67.047,20.172C67.047,18.11 65.422,16.438 63.359,16.422L46.469,16.375C44.422,16.36 42.812,18.094 42.812,20.125C42.812,22.094 44.438,23.891 46.469,23.891Z" style="fill-rule:nonzero;"/>
<path d="M46.469,7.516L63.359,7.578C65.406,7.594 67.047,5.844 67.047,3.828C67.047,1.75 65.422,0.078 63.359,0.063L46.469,0C44.422,-0.016 42.812,1.734 42.812,3.766C42.812,5.75 44.438,7.516 46.469,7.516Z" style="fill-rule:nonzero;"/>
<path d="M9.047,24.594C13.453,24.594 16.984,21.5 16.984,17.063C16.984,12.828 14.078,9.906 10.219,9.906C7.75,9.906 5.719,11 4.703,13.61L5.406,14.063C5.594,9.063 9.016,5.547 14.219,5.203C15.516,5.109 16.5,4.063 16.5,2.781C16.5,1.25 15.172,0.266 13.453,0.266C6.359,0.266 0.047,6.188 0.047,14.406C0.047,20.594 3.797,24.594 9.047,24.594ZM29.297,24.594C33.672,24.594 37.219,21.5 37.219,17.063C37.219,12.828 34.312,9.906 30.453,9.906C27.984,9.906 25.953,11 24.922,13.61L25.641,14.063C25.812,9.063 29.234,5.531 34.422,5.203C35.734,5.109 36.75,4.063 36.75,2.781C36.75,1.25 35.453,0.266 33.688,0.266C26.609,0.266 20.266,6.188 20.266,14.406C20.266,20.594 24.047,24.594 29.297,24.594Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,12.2656,8.9375)">
<path d="M6.125,46.125L23.078,46.125C33.078,46.125 39.469,40.859 39.469,32.844C39.469,26.656 34.562,22.031 28.062,21.766L28.062,21.516C33.484,20.797 37.344,16.781 37.344,11.578C37.344,4.391 31.75,0.016 22.484,0.016L6.125,0.016C2.344,0.016 0,2.234 0,6.109L0,40.047C0,43.922 2.344,46.125 6.125,46.125ZM12.953,37.828L12.953,26.062L18.5,26.062C23.469,26.062 26.312,28.125 26.312,31.844C26.312,35.734 23.922,37.828 18.938,37.828L12.953,37.828ZM12.953,18.844L12.953,8.391L18.219,8.391C22.25,8.391 24.672,10.297 24.672,13.531C24.672,16.828 22.047,18.844 17.672,18.844L12.953,18.844Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.865066,0,0,0.865066,3,10.731571)">
<path d="M21.156,48.391L63.359,48.391C65.406,48.391 67.047,46.656 67.047,44.625C67.047,42.547 65.422,40.875 63.359,40.875L21.156,40.875C19.078,40.875 17.484,42.594 17.484,44.625C17.484,46.609 19.109,48.391 21.156,48.391Z" style="fill-rule:nonzero;"/>
<path d="M5.203,49.828C8.078,49.828 10.422,47.484 10.422,44.625C10.422,41.766 8.078,39.422 5.203,39.422C2.344,39.422 0,41.766 0,44.625C0,47.484 2.344,49.828 5.203,49.828Z" style="fill-rule:nonzero;"/>
<path d="M21.156,28.672L63.359,28.672C65.406,28.672 67.047,26.938 67.047,24.922C67.047,22.844 65.422,21.156 63.359,21.156L21.156,21.156C19.078,21.156 17.484,22.891 17.484,24.922C17.484,26.906 19.109,28.672 21.156,28.672Z" style="fill-rule:nonzero;"/>
<path d="M5.203,30.125C8.078,30.125 10.422,27.797 10.422,24.922C10.422,22.047 8.078,19.719 5.203,19.719C2.344,19.719 0,22.047 0,24.922C0,27.797 2.344,30.125 5.203,30.125Z" style="fill-rule:nonzero;"/>
<path d="M21.156,8.969L63.359,8.969C65.406,8.969 67.047,7.234 67.047,5.203C67.047,3.141 65.422,1.453 63.359,1.453L21.156,1.453C19.078,1.453 17.484,3.172 17.484,5.203C17.484,7.203 19.109,8.969 21.156,8.969Z" style="fill-rule:nonzero;"/>
<path d="M5.203,10.422C8.078,10.422 10.422,8.078 10.422,5.203C10.422,2.344 8.078,0 5.203,0C2.344,0 0,2.344 0,5.203C0,8.078 2.344,10.422 5.203,10.422Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.156042,0,0,0.156042,1.80453,0.721021)">
<rect x="0" y="0" width="53.774" height="67.661" style="fill-opacity:0;"/>
<path d="M10.149,67.641L43.438,67.641C50.137,67.641 53.586,64.135 53.586,57.41L53.586,29.058C53.586,24.717 53.028,22.725 50.308,19.954L33.894,3.284C31.282,0.616 29.111,0 25.212,0L10.149,0C3.481,0 0,3.532 0,10.257L0,57.41C0,64.161 3.455,67.641 10.149,67.641ZM10.637,61.519C7.621,61.519 6.122,59.941 6.122,57.029L6.122,10.637C6.122,7.752 7.621,6.122 10.663,6.122L23.984,6.122L23.984,23.261C23.984,27.733 26.167,29.895 30.619,29.895L47.464,29.895L47.464,57.029C47.464,59.941 45.965,61.519 42.929,61.519L10.637,61.519ZM31.198,24.496C29.903,24.496 29.384,23.945 29.384,22.656L29.384,6.973L46.613,24.496L31.198,24.496Z" style="fill-rule:nonzero;"/>
<g transform="matrix(12.808657,0,0,12.808657,-40.118025,-49.663642)">
<path d="M4.662,8.034C4.545,8.034 4.448,8.015 4.37,7.978C4.293,7.94 4.236,7.89 4.2,7.825C4.163,7.761 4.145,7.69 4.145,7.612C4.145,7.528 4.166,7.454 4.208,7.389C4.25,7.325 4.316,7.274 4.405,7.236C4.495,7.198 4.607,7.178 4.743,7.178L5.084,7.178C5.084,7.115 5.076,7.063 5.06,7.022C5.045,6.981 5.019,6.95 4.984,6.93C4.948,6.909 4.9,6.899 4.838,6.899C4.773,6.899 4.718,6.912 4.673,6.938C4.629,6.964 4.601,7.005 4.59,7.061L4.187,7.061C4.197,6.961 4.23,6.873 4.286,6.798C4.343,6.724 4.419,6.665 4.514,6.622C4.609,6.58 4.718,6.558 4.841,6.558C4.975,6.558 5.092,6.58 5.19,6.624C5.289,6.668 5.366,6.731 5.421,6.815C5.476,6.899 5.503,7.003 5.503,7.128L5.503,8L5.154,8L5.104,7.796C5.083,7.831 5.059,7.864 5.031,7.892C5.003,7.921 4.971,7.946 4.933,7.968C4.896,7.989 4.855,8.006 4.81,8.017C4.766,8.028 4.716,8.034 4.662,8.034ZM4.766,7.715C4.81,7.715 4.849,7.708 4.883,7.693C4.917,7.678 4.945,7.657 4.97,7.631C4.994,7.605 5.014,7.575 5.03,7.54C5.046,7.506 5.057,7.468 5.065,7.427L5.065,7.424L4.794,7.424C4.747,7.424 4.708,7.43 4.678,7.443C4.647,7.455 4.624,7.472 4.609,7.494C4.594,7.517 4.587,7.543 4.587,7.572C4.587,7.604 4.595,7.631 4.611,7.652C4.626,7.674 4.648,7.689 4.675,7.7C4.702,7.71 4.732,7.715 4.766,7.715Z" style="fill-rule:nonzero;"/>
<path d="M5.835,7.521L6.43,7.521L6.077,8.432L5.637,8.432L5.835,7.521Z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.124,0,0,0.124,1.867256,1)">
<path d="M69.43,159.719C69.43,125.199 97.41,97.219 131.922,97.219L486.012,97.219L486.012,458.328C486.012,481.34 467.359,500 444.352,500L152.738,500C106.73,500 69.43,462.691 69.43,416.672L69.43,159.719Z" style="fill:rgb(38,120,43);fill-rule:nonzero;"/>
<path d="M69.43,83.328C69.43,37.309 106.73,0 152.738,0L319.371,0L319.371,166.672L152.738,166.672C106.73,166.672 69.43,203.98 69.43,250L69.43,83.328Z" style="fill:rgb(115,218,95);fill-rule:nonzero;"/>
<g transform="matrix(1.243478,0,0,1.243478,-118.330239,0)">
<path d="M332.245,0L444.34,0C467.348,0 486,18.652 486,41.66L486,125.012C486,148.02 467.348,166.672 444.34,166.672L332.245,166.672C309.237,166.672 290.585,148.02 290.585,125.012L290.585,41.66C290.585,18.652 309.237,0 332.245,0Z" style="fill:rgb(143,233,111);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1.31598,0,0,1.31598,0,-144.822643)">
<path d="M45.129,236.109L177.039,236.109C201.965,236.109 222.172,256.316 222.172,281.238L222.172,413.199C222.172,438.125 201.965,458.328 177.039,458.328L45.129,458.328C20.207,458.328 0,438.125 0,413.199L0,281.238C0,256.316 20.207,236.109 45.129,236.109Z" style="fill:rgb(11,78,46);fill-rule:nonzero;"/>
<g transform="matrix(1.2548,0,0,1.2548,-33.009557,-88.471875)">
<path d="M169.48,410.711L135.23,410.711L113.73,370.238C112.961,368.82 112.371,367.699 111.961,366.871C111.609,365.988 111.219,364.98 110.809,363.859L110.461,363.859C109.93,365.281 109.43,366.43 108.961,367.309C108.488,368.199 107.93,369.289 107.281,370.59L84.98,410.699L52.68,410.699L91.441,347.121L55.34,283.719L89.141,283.719L108.25,319.852C109.02,321.328 109.672,322.629 110.199,323.75C110.789,324.809 111.379,326.078 111.969,327.559L112.32,327.559C113.141,325.852 113.789,324.488 114.27,323.488C114.801,322.488 115.512,321.16 116.391,319.512L136.211,283.738L168.422,283.738L131.789,346.172L169.488,410.719L169.48,410.711Z" style="fill:rgb(254,254,254);fill-rule:nonzero;"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.760492,0,0,0.760492,9.844746,-1.023725)">
<path d="M19.578,35.125L14.438,35.125C10.359,35.125 8.062,37.422 8.062,41.5L8.062,64.734C8.062,68.828 10.359,71.125 14.438,71.125L43.828,71.125C47.906,71.125 50.203,68.828 50.203,64.734L50.203,41.5C50.203,37.422 47.906,35.125 43.828,35.125L38.672,35.125L38.672,27.062L44.266,27.062C53.234,27.062 58.266,32.062 58.266,41.047L58.266,65.188C58.266,74.156 53.234,79.188 44.266,79.188L14,79.188C5.016,79.188 0,74.156 0,65.188L0,41.047C0,32.062 5.016,27.062 14,27.062L19.578,27.062L19.578,35.125Z" style="fill-rule:nonzero;"/>
<path d="M32.423,14.141L32.75,18.578L32.75,49.219C32.75,51.125 31.141,52.766 29.125,52.766C27.109,52.766 25.516,51.125 25.516,49.219L25.516,18.578L25.851,14.122L29.125,9.438L32.423,14.141Z" style="fill-rule:nonzero;"/>
<path d="M18.172,21.328C19.078,21.328 19.938,20.953 20.547,20.297L24.375,16.234L29.125,9.438L33.891,16.234L37.703,20.297C38.312,20.953 39.156,21.328 40.062,21.328C41.703,21.328 43.188,20.094 43.188,18.328C43.188,17.406 42.859,16.734 42.234,16.109L31.984,6.281C31.047,5.359 30.109,5.031 29.125,5.031C28.156,5.031 27.234,5.359 26.266,6.281L16.031,16.109C15.422,16.734 15.062,17.406 15.062,18.328C15.062,20.094 16.531,21.328 18.172,21.328Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.011907,-0,-0,-0.011907,-0.017623,64)">
<path d="M2168,4709C1894,4666 1703,4555 1616,4389C1580,4321 1580,4184 1616,4116C1686,3983 1795,3902 1990,3838C2374,3713 2898,3806 3075,4030C3139,4112 3155,4156 3155,4253C3155,4349 3139,4393 3075,4475C2930,4659 2521,4765 2168,4709ZM2531,4395C2672,4372 2780,4328 2829,4275C2848,4253 2848,4252 2829,4230C2762,4157 2554,4095 2373,4095C2192,4095 1984,4157 1917,4230C1898,4252 1898,4253 1917,4275C2010,4378 2296,4434 2531,4395Z" style="fill-rule:nonzero;"/>
<path d="M4413.328,0.003L3970,0C2805,0 2804,0 2641,56C2546.293,88.172 2456.943,133.018 2375.17,188.649L752.515,188.649C440.617,188.649 187.395,441.871 187.395,753.769L187.395,2042.949C187.395,2333.828 407.638,2573.672 690.318,2604.678C623.112,2767.055 626.105,2931.521 701,3087L731,3149L707,3196C648,3311 626,3466 653,3579L665,3631L598,3692C381,3894 287,4149 343,4391C403,4658 606,4880 958,5066C1270,5230 1637,5326 2121,5369C2228,5379 2689,5366 2814,5350C3439,5270 3943,5054 4214,4748C4509,4415 4483,4003 4148,3692L4081,3631L4093,3579C4120,3465 4098,3312 4038,3194C4014,3145 4014,3144 4033,3113C4078,3041 4100,2950 4100,2835C4100,2738.782 4090.9,2682.466 4058.642,2608.068L4625.617,2608.068C4937.515,2608.068 5190.736,2354.846 5190.736,2042.949L5190.736,753.769C5190.736,611.423 5137.993,481.298 5051,381.889L5051.021,155.223C5051.021,69.541 4981.4,0.003 4895.817,0.003L4413.328,0.003ZM1063.992,2608.068L1634.782,2608.068C1394.851,2668.367 1190.699,2752.015 1034,2856C966,2901 967,2901 960,2872C944,2809 979,2712 1045,2630C1050.866,2622.695 1057.204,2615.38 1063.992,2608.068ZM1078,3393C1025,3416 976,3438 970,3440C961,3444 960,3436 965,3411C1016,3186 1382,2969 1856,2883C2036,2850 2146,2841 2373,2841C2600,2841 2710,2850 2890,2883C3364,2969 3730,3186 3781,3411C3786,3436 3785,3444 3776,3440C3770,3438 3721,3416 3668,3393C3424,3286 3132,3213 2772,3169C2627,3151 2119,3151 1974,3169C1614,3213 1322,3286 1078,3393ZM4854.788,2042.949C4854.788,2169.432 4752.1,2272.12 4625.617,2272.12L752.515,2272.12C626.032,2272.12 523.344,2169.432 523.344,2042.949L523.344,753.769C523.344,627.286 626.032,524.597 752.515,524.597L4625.617,524.597C4752.1,524.597 4854.788,627.286 4854.788,753.769L4854.788,2042.949ZM2621,5050C1966,5103 1283,4947 897,4656C795,4579 733,4507 685,4411C650,4342 646,4324 646,4253C646,4146 685,4063 783,3960C1103,3621 1884,3416 2631,3476C3229,3524 3721,3702 3963,3960C4061,4063 4100,4146 4100,4253C4100,4324 4096,4342 4061,4411C4013,4507 3951,4579 3849,4656C3560,4873 3138,5009 2621,5050ZM3111.507,2608.068L3680.692,2608.068C3712.673,2642.169 3737.999,2677.689 3757,2715C3782,2766 3797,2857 3783,2881C3775,2896 3769,2895 3733,2870C3569.742,2759.218 3359.738,2670.745 3111.507,2608.068Z"/>
<g transform="matrix(143.743961,-0,-0,-143.743961,-3687.186324,8190.967756)">
<path d="M34.865,52.259C37.413,52.259 38.935,51.026 38.935,49.082C38.935,47.56 37.996,46.722 35.931,46.326L34.939,46.139C33.887,45.938 33.451,45.656 33.451,45.147C33.451,44.577 33.974,44.174 34.865,44.174C35.576,44.174 36.112,44.409 36.428,45.012C36.716,45.482 37.051,45.676 37.587,45.676C38.204,45.669 38.62,45.287 38.62,44.717C38.62,44.516 38.586,44.355 38.519,44.188C38.05,42.961 36.696,42.25 34.845,42.25C32.62,42.25 31.004,43.45 31.004,45.314C31.004,46.789 32.01,47.734 33.934,48.096L34.933,48.284C36.079,48.505 36.488,48.78 36.488,49.316C36.488,49.906 35.864,50.335 34.906,50.335C34.128,50.335 33.478,50.081 33.149,49.477C32.834,48.995 32.512,48.827 32.036,48.827C31.413,48.827 30.97,49.243 30.97,49.859C30.97,50.061 31.011,50.268 31.098,50.469C31.5,51.468 32.767,52.259 34.865,52.259Z" style="fill-rule:nonzero;"/>
<path d="M41.308,52.239C42.086,52.239 42.535,51.777 42.535,50.959L42.535,49.357L43.433,48.418L45.941,51.549C46.336,52.052 46.691,52.246 47.208,52.246C47.878,52.246 48.388,51.73 48.388,51.059C48.388,50.718 48.22,50.349 47.818,49.853L45.344,46.823L47.657,44.382C47.985,44.027 48.113,43.759 48.113,43.397C48.113,42.767 47.596,42.264 46.953,42.264C46.537,42.264 46.229,42.425 45.88,42.814L42.589,46.46L42.535,46.46L42.535,43.551C42.535,42.733 42.086,42.27 41.308,42.27C40.53,42.27 40.075,42.733 40.075,43.551L40.075,50.959C40.075,51.777 40.53,52.239 41.308,52.239Z" style="fill-rule:nonzero;"/>
<path d="M53.583,52.259C56.097,52.259 57.746,50.832 57.746,48.659L57.746,43.551C57.746,42.733 57.297,42.27 56.513,42.27C55.735,42.27 55.286,42.733 55.286,43.551L55.286,48.398C55.286,49.524 54.676,50.195 53.583,50.195C52.484,50.195 51.874,49.524 51.874,48.398L51.874,43.551C51.874,42.733 51.424,42.27 50.647,42.27C49.869,42.27 49.413,42.733 49.413,43.551L49.413,48.659C49.413,50.832 51.062,52.259 53.583,52.259Z" style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.889695,0,0,0.889695,3,3.004404)">
<path d="M65.191,32.59C65.191,50.56 50.592,65.181 32.59,65.181C14.621,65.181 0,50.56 0,32.59C0,14.599 14.621,0 32.59,0C50.592,0 65.191,14.599 65.191,32.59ZM24.761,42.45C22.996,42.45 21.767,43.565 21.767,45.227C21.767,46.89 22.996,48.026 24.761,48.026L40.494,48.026C42.259,48.026 43.509,46.89 43.509,45.227C43.509,43.565 42.259,42.45 40.494,42.45L24.761,42.45ZM20.339,31.642C18.552,31.642 17.323,32.777 17.323,34.44C17.323,36.144 18.552,37.249 20.339,37.249L44.937,37.249C46.703,37.249 47.931,36.144 47.931,34.44C47.931,32.777 46.703,31.642 44.937,31.642L20.339,31.642ZM16.054,20.886C14.279,20.886 13.039,22.001 13.039,23.684C13.039,25.358 14.279,26.472 16.054,26.472L49.222,26.472C50.976,26.472 52.237,25.358 52.237,23.684C52.237,22.001 50.976,20.886 49.222,20.886L16.054,20.886Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.889695,0,0,0.889695,3,3.004404)">
<path d="M32.59,65.181C50.592,65.181 65.191,50.582 65.191,32.59C65.191,14.599 50.592,0 32.59,0C14.599,0 0,14.599 0,32.59C0,50.582 14.599,65.181 32.59,65.181ZM32.59,58.02C18.529,58.02 7.161,46.652 7.161,32.59C7.161,18.529 18.529,7.161 32.59,7.161C46.652,7.161 58.03,18.529 58.03,32.59C58.03,46.652 46.652,58.02 32.59,58.02Z" style="fill-rule:nonzero;"/>
<path d="M16.733,26.802L48.5,26.802C50.183,26.802 51.362,25.748 51.362,24.146C51.362,22.565 50.183,21.511 48.5,21.511L16.733,21.511C15.029,21.511 13.871,22.565 13.871,24.146C13.871,25.748 15.029,26.802 16.733,26.802ZM20.832,37.096L44.402,37.096C46.065,37.096 47.233,36.042 47.233,34.44C47.233,32.869 46.065,31.805 44.402,31.805L20.832,31.805C19.147,31.805 17.98,32.869 17.98,34.44C17.98,36.042 19.147,37.096 20.832,37.096ZM25.068,47.411L40.145,47.411C41.808,47.411 42.996,46.336 42.996,44.755C42.996,43.184 41.808,42.121 40.145,42.121L25.068,42.121C23.405,42.121 22.237,43.184 22.237,44.755C22.237,46.336 23.405,47.411 25.068,47.411Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.929807,0,0,0.929807,1,12.754528)">
<path d="M16.503,41.397L50.239,41.397C52.648,41.397 54.536,39.324 54.536,36.987C54.536,34.578 52.671,32.569 50.239,32.569L16.503,32.569C14.094,32.569 12.212,34.626 12.212,36.987C12.212,39.276 14.118,41.397 16.503,41.397Z" style="fill-rule:nonzero;"/>
<path d="M10.407,25.112L56.342,25.112C58.727,25.112 60.608,23.039 60.608,20.702C60.608,18.269 58.751,16.285 56.342,16.285L10.407,16.285C8.022,16.285 6.133,18.341 6.133,20.702C6.133,22.992 8.046,25.112 10.407,25.112Z" style="fill-rule:nonzero;"/>
<path d="M4.267,8.827L62.414,8.827C64.823,8.827 66.68,6.755 66.68,4.417C66.68,1.984 64.847,0 62.414,0L4.267,0C1.882,0 0,2.032 0,4.417C0,6.683 1.905,8.827 4.267,8.827Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.624686,0,0,0.624686,3,17.237995)">
<path d="M45.17,47.212C46.58,47.212 47.578,46.42 47.578,45.039C47.578,44.432 47.455,44.033 47.022,43.038C43.627,36.904 41.807,30.56 41.807,23.653C41.807,16.937 43.541,10.39 47.022,4.224C47.455,3.24 47.578,2.84 47.578,2.233C47.578,0.895 46.58,0.05 45.17,0.05C43.622,0.05 42.412,0.807 41.094,2.698C36.763,8.399 34.637,15.598 34.637,23.632C34.637,31.696 36.75,38.726 41.094,44.574C42.402,46.475 43.622,47.212 45.17,47.212ZM53.441,39.915C54.789,39.915 55.627,39.472 56.736,37.85L63.381,28.189L63.526,28.189L70.367,38.036C71.337,39.448 72.177,39.915 73.434,39.915C75.47,39.915 76.876,38.574 76.876,36.677C76.876,35.855 76.63,35.133 76.075,34.383L68.175,23.647L76.006,13.227C76.629,12.367 76.927,11.646 76.927,10.722C76.927,8.851 75.474,7.596 73.587,7.596C72.104,7.596 71.3,8.297 70.379,9.72L64.033,19.158L63.877,19.158L57.501,9.689C56.548,8.256 55.66,7.596 54.075,7.596C52.07,7.596 50.568,9.08 50.568,10.866C50.568,11.932 50.899,12.683 51.435,13.403L59.08,23.63L51.105,34.459C50.429,35.36 50.226,36.071 50.226,36.964C50.226,38.669 51.625,39.915 53.441,39.915ZM82.304,47.212C83.862,47.212 85.062,46.465 86.38,44.574C90.754,38.736 92.847,31.696 92.847,23.632C92.847,15.598 90.682,8.419 86.38,2.698C85.072,0.797 83.862,0.05 82.304,0.05C80.894,0.05 79.896,0.895 79.896,2.233C79.896,2.84 80.029,3.24 80.452,4.224C83.933,10.39 85.667,16.937 85.667,23.653C85.667,30.56 83.848,36.904 80.452,43.038C80.029,44.033 79.896,44.432 79.896,45.039C79.896,46.377 80.894,47.212 82.304,47.212Z" style="fill-rule:nonzero;"/>
<path d="M5.252,47.17C12.053,47.17 15.203,44.455 16.783,37.027L20.127,21.046L25.664,21.046C27.685,21.046 29.099,19.888 29.099,17.909C29.099,16.108 27.887,15.025 26.141,15.025L21.395,15.025L22.161,11.302C22.935,7.691 24.066,6.295 27.165,6.295C27.697,6.295 28.187,6.263 28.56,6.222C30.37,5.986 31.317,5.049 31.317,3.437C31.317,1.14 29.52,0.071 25.884,0.071C19.19,0.071 15.859,3.04 14.354,10.214L13.352,15.025L9.606,15.025C7.544,15.025 6.149,16.193 6.149,18.163C6.149,19.974 7.342,21.046 9.139,21.046L12.084,21.046L8.945,35.949C8.159,39.645 7.007,40.946 3.961,40.946C3.534,40.946 3.096,40.987 2.775,41.019C0.93,41.266 0,42.325 0,43.856C0,46.1 1.765,47.17 5.252,47.17Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,14.41405,8.929688)">
<path d="M4.047,46.125L24.406,46.125C26.797,46.125 28.438,44.75 28.438,42.391C28.438,40.094 26.859,38.719 24.422,38.719L19.062,38.719L25.797,7.438L31.156,7.438C33.547,7.438 35.172,6.062 35.172,3.719C35.172,1.406 33.594,0.016 31.172,0.016L10.75,0.016C8.328,0.016 6.734,1.406 6.734,3.719C6.734,6.062 8.375,7.438 10.766,7.438L16.125,7.438L9.391,38.719L4.031,38.719C1.594,38.719 0,40.094 0,42.391C0,44.75 1.641,46.125 4.047,46.125Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 979 B

View File

@ -1,10 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g>
<path d="M12.892,61L51.107,61C57.723,61 60.999,57.755 60.999,51.265L60.999,12.766C60.999,6.276 57.723,3 51.107,3L12.892,3C6.308,3 3,6.276 3,12.766L3,51.265C3,57.755 6.308,61 12.892,61ZM12.955,55.928C9.805,55.928 8.072,54.258 8.072,50.982L8.072,13.05C8.072,9.774 9.805,8.072 12.955,8.072L51.044,8.072C54.163,8.072 55.927,9.773 55.927,13.05L55.927,50.982C55.927,54.258 54.163,55.928 51.044,55.928L12.955,55.928Z" style="fill-rule:nonzero;"/>
<g transform="matrix(0.630094,0,0,0.630094,12.050043,9.166267)">
<path d="M14.688,30.601L14.688,17.094C14.688,15.264 15.627,13.647 17.191,12.746L29.089,5.879C30.674,4.944 32.523,4.944 34.111,5.878L46.001,12.745C47.566,13.647 48.504,15.264 48.504,17.094L48.504,30.521L60.25,37.305C61.815,38.206 62.754,39.824 62.754,41.653L62.754,55.39C62.754,57.22 61.813,58.84 60.251,59.729L48.363,66.604C46.769,67.533 44.919,67.532 43.339,66.605L31.662,59.857L19.995,66.604C18.401,67.533 16.552,67.532 14.971,66.605L3.077,59.732C1.511,58.84 0.57,57.22 0.57,55.39L0.57,41.653C0.57,39.824 1.509,38.206 3.073,37.306L14.688,30.601ZM22.461,16.011L31.554,21.217L40.687,15.981L32,10.966L31.986,10.958C31.74,10.809 31.453,10.809 31.207,10.958L31.193,10.966L22.461,16.011ZM28.869,35.565L28.869,25.991L20.135,20.989L20.135,30.103C20.135,30.312 20.215,30.501 20.356,30.648L28.869,35.565ZM43.118,60.173L43.118,50.551L34.386,45.55L34.386,54.723C34.406,54.993 34.565,55.222 34.804,55.364L43.118,60.173ZM20.205,60.177L28.527,55.364C28.782,55.213 28.931,54.957 28.931,54.662L28.931,45.503L20.205,50.498L20.205,60.177ZM14.751,60.174L14.751,50.551L6.017,45.549L6.017,54.662C6.017,54.958 6.179,55.212 6.437,55.364L14.751,60.174ZM34.324,35.617L42.645,30.805C42.901,30.653 43.05,30.398 43.05,30.103L43.05,20.943L34.324,25.938L34.324,35.617ZM36.709,40.57L45.803,45.777L54.936,40.54L46.249,35.526L46.234,35.517C45.989,35.368 45.702,35.368 45.457,35.517L45.442,35.526L36.709,40.57ZM48.573,60.177L56.895,55.364C57.15,55.213 57.299,54.957 57.299,54.662L57.299,45.503L48.573,50.498L48.573,60.177ZM8.341,40.57L17.436,45.777L26.526,40.564L17.621,35.418L17.618,35.417C17.44,35.381 17.256,35.417 17.09,35.517L17.075,35.526L8.341,40.57Z"/>
<g transform="matrix(0.58577,0,0,0.58577,29.000019,28.999974)">
<path d="M11.031,59.75L48.719,59.75C55.859,59.75 59.75,55.891 59.75,48.797L59.75,10.969C59.75,3.891 55.859,-0 48.719,-0L11.031,-0C3.906,-0 -0,3.891 -0,10.969L-0,48.797C-0,55.891 3.906,59.75 11.031,59.75ZM11.906,51.688C9.391,51.688 8.063,50.469 8.063,47.813L8.063,11.969C8.063,9.313 9.391,8.078 11.906,8.078L47.844,8.078C50.344,8.078 51.688,9.313 51.688,11.969L51.688,47.813C51.688,50.469 50.344,51.688 47.844,51.688L11.906,51.688Z" style="fill-rule:nonzero;"/>
<g transform="matrix(0.570497,0,0,0.570497,16.522802,13.725853)">
<path d="M6.499,56.236L41.124,56.236C44.294,56.236 46.809,54.281 46.809,50.825C46.809,47.443 44.391,45.464 41.124,45.464L18.117,45.464L18.117,45.062C22.049,43.001 23.774,38.335 23.774,33.596C23.774,32.751 23.669,32.057 23.534,31.399L37.509,31.399C39.584,31.399 41.06,30.032 41.06,28.125C41.06,26.241 39.584,24.905 37.509,24.905L22.134,24.905C21.788,23.604 21.265,21.585 21.265,19.395C21.265,13.243 26.441,10.758 32.681,10.758C34.774,10.758 36.433,10.942 37.882,11.247C38.957,11.394 40.282,11.543 41.544,11.543C44.007,11.543 46.13,10.334 46.13,7.354C46.13,5.297 45.211,3.871 43.447,2.745C39.934,0.628 34.458,0.378 30.367,0.378C18.274,0.378 7.903,5.899 7.903,17.414C7.903,19.396 8.212,21.38 9.112,24.905L3.574,24.905C1.5,24.905 0,26.241 0,28.125C0,30.071 1.514,31.399 3.574,31.399L10.467,31.399C10.73,32.486 10.797,33.332 10.797,34.157C10.797,38.814 8.74,42.769 5.065,44.806C3.057,46.118 0.808,47.931 0.808,50.875C0.808,54.298 3.219,56.236 6.499,56.236Z" style="fill-rule:nonzero;"/>
</g>
</g>
<g transform="matrix(1,0,0,1,11.133834,5.394156)">
<rect x="0" y="0" width="74.451" height="58.962" style="fill-opacity:0;"/>
<g transform="matrix(0.700206,0,0,0.700206,-5.133834,5.956054)">
<path d="M28.563,58.962L10.588,58.962C3.679,58.962 0,55.326 0,48.481L0,10.522C0,3.667 3.679,0.02 10.588,0.02L63.676,0.02C70.606,0.02 74.264,3.667 74.264,10.522L74.264,20.933C74.047,20.926 73.827,20.922 73.604,20.922L67.305,20.922L67.305,11.251C67.305,8.378 65.813,6.979 63.097,6.979L11.167,6.979C8.429,6.979 6.959,8.378 6.959,11.251L6.959,47.743C6.959,50.615 8.429,52.003 11.167,52.003L28.563,52.003L28.563,58.962ZM28.857,31.097L16.175,31.097C14.981,31.097 14.107,30.204 14.107,29.051C14.107,27.929 14.981,27.055 16.175,27.055L30.296,27.055C29.634,28.235 29.144,29.582 28.857,31.097ZM16.175,19.207C14.981,19.207 14.107,18.313 14.107,17.138C14.107,16.016 14.981,15.164 16.175,15.164L58.13,15.164C59.295,15.164 60.157,16.016 60.157,17.138C60.157,18.313 59.295,19.207 58.13,19.207L16.175,19.207Z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g>
<path d="M34.148,61L12.892,61C6.308,61 3,57.755 3,51.265L3,12.766C3,6.276 6.308,3 12.892,3L51.107,3C57.723,3 60.999,6.276 60.999,12.766L60.999,34.148C59.513,32.753 57.802,31.593 55.927,30.73L55.927,13.05C55.927,9.773 54.163,8.072 51.044,8.072L12.955,8.072C9.805,8.072 8.072,9.774 8.072,13.05L8.072,50.982C8.072,54.258 9.805,55.928 12.955,55.928L30.73,55.928C31.593,57.803 32.752,59.514 34.148,61ZM29.009,48.611L24.649,51.133C23.644,51.718 22.48,51.718 21.483,51.134L13.989,46.803C13.002,46.241 12.409,45.22 12.409,44.067L12.409,35.412C12.409,34.259 13.001,33.24 13.987,32.672L21.305,28.448L21.305,19.937C21.305,18.784 21.897,17.765 22.882,17.197L30.379,12.87C31.377,12.281 32.542,12.281 33.543,12.87L41.035,17.197C42.021,17.765 42.612,18.784 42.612,19.937L42.612,28.397L44.285,29.364C37.862,30.639 32.6,35.156 30.28,41.135L30.28,37.837L24.781,40.985L24.781,47.084L29.331,44.452C29.113,45.602 28.999,46.788 28.999,48C28.999,48.205 29.003,48.408 29.009,48.611ZM33.677,31.608L38.92,28.576C39.082,28.481 39.176,28.32 39.176,28.134L39.176,22.362L33.677,25.51L33.677,31.608ZM26.202,19.254L31.932,22.535L37.687,19.236L32.213,16.076L32.204,16.071C32.049,15.977 31.868,15.977 31.713,16.071L31.705,16.076L26.202,19.254ZM17.306,34.729L23.036,38.01L28.764,34.726L23.153,31.483L23.151,31.483C23.039,31.46 22.923,31.482 22.818,31.546L22.809,31.551L17.306,34.729ZM30.241,31.576L30.241,25.543L24.737,22.391L24.737,28.134C24.737,28.266 24.788,28.384 24.876,28.477L30.241,31.576ZM21.345,47.081L21.345,41.018L15.841,37.866L15.841,43.608C15.841,43.795 15.944,43.955 16.106,44.051L21.345,47.081Z"/>
<g transform="matrix(1.077085,0,0,1.077085,-2.466684,-4.933344)">
<path d="M46.854,34.29C55.053,34.29 61.709,40.946 61.709,49.145C61.709,57.344 55.053,64 46.854,64C38.656,64 32,57.344 32,49.145C32,40.946 38.656,34.29 46.854,34.29ZM46.854,38.468C40.962,38.468 36.177,43.252 36.177,49.145C36.177,55.038 40.962,59.822 46.854,59.822C52.747,59.822 57.531,55.038 57.531,49.145C57.531,43.252 52.747,38.468 46.854,38.468Z"/>
</g>
<g transform="matrix(1,0,0,1,-1.649781,-7.506927)">
<circle cx="49.627" cy="50.064" r="3.017"/>
</g>
<g transform="matrix(1,0,0,1,-6.45365,0.751087)">
<circle cx="49.627" cy="50.064" r="3.017"/>
</g>
<g transform="matrix(1,0,0,1,3.071774,0.733775)">
<circle cx="49.627" cy="50.064" r="3.017"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.905936,0,0,0.905936,8.091803,1)">
<path d="M0,64.406C0,66.625 1.812,68.438 4.031,68.438C6.25,68.438 8.062,66.625 8.062,64.406L8.062,9.844C8.062,8.75 8.75,8.062 9.797,8.062L42.984,8.062C44.031,8.062 44.719,8.75 44.719,9.844L44.719,64.406C44.719,66.625 46.531,68.438 48.766,68.438C50.984,68.438 52.781,66.625 52.781,64.406L52.781,8.828C52.781,3.391 49.391,0 43.875,0L8.922,0C3.406,0 0,3.391 0,8.828L0,64.406Z" style="fill-rule:nonzero;"/>
<path d="M11.891,65.75C11.891,66.547 12.5,66.938 13.359,66.578L22.859,62.547C23.672,62.203 24.031,61.828 24.031,61.062L24.031,17.438C24.031,16.672 23.672,16.281 22.875,15.969L13.359,11.906C12.5,11.562 11.891,11.953 11.891,12.766L11.891,65.75Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,0,-0.113382)">
<g transform="matrix(0.725404,0,0,0.725404,3.539221,11.473342)">
<path d="M43.789,56.563L11.031,56.562C3.922,56.562 0,52.672 0,45.594L0,11C0,3.922 3.906,0.031 10.516,0.031L67.422,0.031C74.547,0.031 78.469,3.922 78.469,11L78.469,39.642C76.475,37.218 73.921,35.265 71.016,33.991L71.016,11.879L54.327,26.905L60.012,32.59C57.573,32.984 55.276,33.831 53.211,35.039L49.46,31.288L45.719,34.656C43.594,36.578 41.609,37.391 39.219,37.391C36.844,37.391 34.844,36.578 32.734,34.656L28.993,31.29L11.188,49.108L11.328,49.109L43.533,49.109C43.374,50.11 43.292,51.133 43.292,52.174C43.292,53.679 43.464,55.148 43.789,56.563ZM7.453,43.594L24.132,26.915L7.453,11.906L7.453,43.594ZM12.279,7.484L36.734,29.547C37.547,30.266 38.359,30.641 39.219,30.641C40.094,30.641 40.906,30.266 41.719,29.547L66.174,7.484L12.279,7.484Z"/>
</g>
<g transform="matrix(0.692828,0,0,0.692828,-0,0.822733)">
<path d="M87.672,70C87.672,78.984 80.188,86.469 71.219,86.469C62.203,86.469 54.766,79.016 54.766,70C54.766,61 62.203,53.562 71.219,53.562C80.234,53.562 87.672,61 87.672,70ZM75.922,62.812L68.984,72.391L65.766,68.766C65.281,68.219 64.609,67.938 63.797,67.938C62.391,67.938 61.062,68.906 61.062,70.641C61.062,71.328 61.391,72.031 61.891,72.578L67.047,78.156C67.594,78.766 68.453,79 69.188,79C70.078,79 70.938,78.625 71.391,78.016L80.375,65.828C80.719,65.344 80.875,64.781 80.875,64.297C80.875,62.812 79.672,61.625 78.172,61.625C77.266,61.625 76.453,62.078 75.922,62.812Z" style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.725404,0,0,0.725404,3.539221,11.473342)">
<path d="M11.031,56.562C3.922,56.562 0,52.672 0,45.594L0,11C0,3.922 3.906,0.031 10.516,0.031L67.422,0.031C74.547,0.031 78.469,3.922 78.469,11L78.469,45.594C78.469,52.672 74.562,56.562 67.953,56.562L11.031,56.562ZM7.453,43.594L24.132,26.915L7.453,11.906L7.453,43.594ZM67.279,49.107L49.46,31.288L45.719,34.656C43.594,36.578 41.609,37.391 39.219,37.391C36.844,37.391 34.844,36.578 32.734,34.656L28.993,31.29L11.188,49.108C11.234,49.109 11.281,49.109 11.328,49.109L67.109,49.109C67.167,49.109 67.223,49.109 67.279,49.107ZM71.016,11.879L54.327,26.905L71.016,43.594L71.016,11.879ZM12.279,7.484L36.734,29.547C37.547,30.266 38.359,30.641 39.219,30.641C40.094,30.641 40.906,30.266 41.719,29.547L66.174,7.484L12.279,7.484Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" version="1.1" viewBox="0 0 72 69" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<path d="m20.601 66.402v-19.472h8.932v19.472h28.517c4.163 0 6.597-2.375 6.597-6.48v-23.76c3.033-1.711 5.043-4.945 5.043-8.699v-0.498c0-1.144-0.293-2.17-0.88-3.167l-7.77-13.283v-2.433c0-3.724-2.287-5.952-6.069-5.952h-37.912c-3.783 0-6.099 2.228-6.099 5.952v2.433l-7.77 13.283c-0.587 0.997-0.88 2.023-0.88 3.167v0.498c0 3.754 2.01 6.988 5.043 8.699v23.76c0 4.105 2.434 6.48 6.597 6.48h6.651zm39.325-28.945c-0.098 3e-3 -0.195 5e-3 -0.293 5e-3 -3.255 0-6.099-1.496-7.858-3.871-1.789 2.375-4.604 3.871-7.888 3.871s-6.128-1.496-7.887-3.871c-1.759 2.375-4.603 3.871-7.887 3.871-3.255 0-6.099-1.496-7.858-3.871-1.789 2.375-4.633 3.871-7.888 3.871-0.098 0-0.195-2e-3 -0.293-5e-3v21.262c0 1.906 1.026 2.962 2.903 2.962h1.759v-16.449c0-1.349 0.909-2.229 2.258-2.229h12.432c1.349 0 2.228 0.88 2.228 2.229v16.449h23.399c1.876 0 2.873-1.056 2.873-2.962v-21.262zm-37.443-8.088h11.289c-0.645 2.375-2.639 3.958-5.659 3.958-2.991 0-5.014-1.583-5.63-3.958zm31.52 0h11.289c-0.645 2.375-2.639 3.958-5.659 3.958-2.991 0-5.014-1.583-5.63-3.958zm-47.295 0h11.318c-0.645 2.375-2.639 3.958-5.659 3.958s-5.014-1.583-5.659-3.958zm31.52 0h11.318c-0.645 2.375-2.639 3.958-5.659 3.958s-5.013-1.583-5.659-3.958zm-30.846-4.134 6.363-11.201h44.275l6.539 11.201h-57.177zm8.005-15.629v-0.703c0-1.466 0.851-2.346 2.287-2.346h36.652c1.437 0 2.287 0.88 2.287 2.346v0.703h-41.226z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.865066,0,0,0.865066,3,8.230968)">
<path d="M23.234,51.266L63.359,51.266C65.406,51.266 67.047,49.531 67.047,47.516C67.047,45.422 65.422,43.75 63.359,43.75L23.234,43.75C21.172,43.75 19.562,45.469 19.562,47.516C19.562,49.484 21.188,51.266 23.234,51.266Z" style="fill-rule:nonzero;"/>
<path d="M5.922,54.953C10.047,54.953 12.406,53.203 12.406,50.391C12.406,48.562 11.125,47.344 8.859,47.141L8.859,47.031C10.422,46.734 11.797,45.594 11.797,43.688C11.797,41.078 9.203,39.734 5.906,39.734C3.328,39.734 0.391,40.859 0.391,43.172C0.391,44.156 1.094,44.859 2.25,44.859C2.984,44.859 3.344,44.578 3.828,44.125C4.625,43.375 5.203,43.156 5.906,43.156C6.781,43.156 7.484,43.547 7.484,44.422C7.484,45.219 6.812,45.656 5.641,45.656L5.219,45.656C4.297,45.656 3.688,46.172 3.688,47.203C3.688,48.188 4.266,48.734 5.219,48.734L5.672,48.734C6.922,48.734 7.609,49.172 7.625,50.047C7.641,50.812 6.891,51.359 5.922,51.359C4.812,51.359 4,50.672 3.406,50.188C3.016,49.891 2.656,49.609 2,49.609C0.828,49.609 0,50.297 0,51.438C0,53.688 3.094,54.953 5.922,54.953Z" style="fill-rule:nonzero;"/>
<path d="M23.234,31.562L63.359,31.562C65.406,31.562 67.047,29.812 67.047,27.797C67.047,25.719 65.422,24.047 63.359,24.047L23.234,24.047C21.172,24.047 19.562,25.766 19.562,27.797C19.562,29.781 21.188,31.562 23.234,31.562Z" style="fill-rule:nonzero;"/>
<path d="M2.031,34.797L10.5,34.797C11.531,34.797 12.312,34.141 12.312,33.062C12.312,32 11.547,31.312 10.5,31.312L6.219,31.312L6.219,31.203L8.719,29.234C10.859,27.531 11.875,26.531 11.875,24.5C11.875,21.797 9.562,20.031 5.859,20.031C2.641,20.031 0.344,21.609 0.344,23.703C0.344,24.828 1.078,25.453 2.297,25.453C3.062,25.453 3.594,25.203 4.172,24.484C4.688,23.828 5.172,23.5 5.938,23.5C6.75,23.5 7.359,24.016 7.359,24.844C7.359,25.547 6.953,26.125 5.312,27.438L1.109,30.859C0.406,31.422 0.109,32.062 0.109,32.891C0.109,34 0.859,34.797 2.031,34.797Z" style="fill-rule:nonzero;"/>
<path d="M23.234,11.844L63.359,11.844C65.406,11.844 67.047,10.109 67.047,8.094C67.047,6.016 65.422,4.328 63.359,4.328L23.234,4.328C21.172,4.328 19.562,6.047 19.562,8.094C19.562,10.078 21.188,11.844 23.234,11.844Z" style="fill-rule:nonzero;"/>
<path d="M7.312,15.234C8.688,15.234 9.656,14.469 9.656,12.766L9.656,2.469C9.656,0.953 8.562,0 6.984,0C5.766,0 4.969,0.406 4.031,1.016L1.812,2.516C1.094,2.984 0.766,3.438 0.766,4.203C0.766,5.172 1.453,5.828 2.281,5.797C2.703,5.781 2.922,5.734 3.516,5.359L4.828,4.469L4.922,4.469L4.922,12.766C4.922,14.469 5.875,15.234 7.312,15.234Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.069972,0,0,0.069972,-2.986098,-11.802594)">
<path d="M242,226C206,232 174.833,248.333 148.5,275C122.167,301.667 106,333 100,369L100,884C106,919.333 122,950 148,976C174,1002 204.333,1018.333 239,1025L243,1026L758,1026L761,1025C796.333,1018.333 826.833,1001.833 852.5,975.5C878.167,949.167 894,918.333 900,883L900,368C894,332.667 877.667,301.667 851,275C824.333,248.333 793,232 757,226L242,226Z" style="fill:rgb(255,152,0);fill-rule:nonzero;"/>
<g transform="matrix(14.291431,0,0,14.291431,42.675613,168.675956)">
<path d="M35.778,26.682C38.344,26.682 42.193,26.705 47.324,26.752L53.061,26.752L53.061,32.07L33.259,32.14L33.259,26.682L35.778,26.682ZM53.131,10.938L53.131,16.256C48.187,16.303 41.563,16.303 33.259,16.256L33.329,11.008L53.131,10.938ZM30.74,18.845L30.74,24.163L11.008,24.093L11.008,18.915L30.74,18.845ZM30.67,11.008C30.717,12.175 30.74,13.924 30.74,16.256L11.008,16.256L11.008,11.008L30.67,11.008ZM18.775,37.178C19.941,37.178 21.073,37.423 22.169,37.913C23.265,38.402 24.186,39.09 24.933,39.977C25.959,41.143 26.542,42.531 26.682,44.14C26.822,45.749 26.542,47.289 25.842,48.758C25.143,50.228 24.093,51.312 22.694,52.012C21.387,52.758 19.93,53.073 18.32,52.956C16.711,52.84 15.265,52.315 13.982,51.382C12.699,50.449 11.848,49.236 11.428,47.744C10.868,46.391 10.752,44.968 11.078,43.475C11.405,41.983 12.081,40.688 13.107,39.592C14.134,38.496 15.37,37.784 16.816,37.458C17.469,37.271 18.122,37.178 18.775,37.178ZM33.259,18.845L53.131,18.845L53.131,24.163L33.259,24.163L33.259,18.845ZM30.74,26.752L30.74,32.14L11.008,32.14L11.008,26.752L30.74,26.752ZM33.259,34.729L53.061,34.729C53.108,35.568 53.108,36.851 53.061,38.577L53.061,39.977L33.329,39.977L33.259,34.729Z" style="fill:white;"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,1.056758,1.088008)">
<rect x="0" y="0" width="59.625" height="59.562" style="fill-opacity:0;"/>
</g>
<g transform="matrix(0.918716,0,0,0.918716,5,4.991502)">
<path d="M17.169,58.796L41.597,58.796C47.087,58.796 51.398,57.182 54.261,54.3C57.207,51.396 58.778,47.075 58.778,41.616L58.778,17.19C58.778,11.722 57.207,7.41 54.261,4.496C51.377,1.602 47.087,0 41.597,0L17.169,0C11.7,0 7.358,1.624 4.496,4.496C1.571,7.41 0,11.722 0,17.19L0,41.616C0,47.075 1.55,51.396 4.496,54.3C7.379,57.204 11.7,58.796 17.169,58.796ZM17.487,51.837C13.94,51.837 11.276,50.962 9.566,49.252C7.813,47.53 6.959,44.898 6.959,41.288L6.959,17.508C6.959,13.909 7.813,11.276 9.566,9.545C11.245,7.865 13.94,6.959 17.487,6.959L41.27,6.959C44.848,6.959 47.49,7.844 49.212,9.545C50.965,11.276 51.819,13.909 51.819,17.508L51.819,41.288C51.819,44.898 50.965,47.53 49.212,49.252C47.511,50.941 44.848,51.837 41.27,51.837L17.487,51.837Z" style="fill-rule:nonzero;"/>
<path d="M34.245,20.733L28.455,25.929L17.907,36.476C17.311,37.063 16.929,37.896 16.929,38.71C16.929,40.544 18.242,41.748 19.997,41.748C20.916,41.748 21.667,41.417 22.323,40.771L32.821,30.304L37.996,24.505C40.497,21.719 37.205,18.138 34.245,20.733ZM36.354,30.119L36.354,34.789C36.354,36.636 37.45,37.853 39.143,37.853C40.836,37.853 41.911,36.582 41.911,34.767L41.911,20.374C41.911,18.014 40.563,16.868 38.364,16.868L23.898,16.868C22.062,16.868 20.845,17.943 20.845,19.636C20.845,21.329 22.064,22.404 23.91,22.404L28.872,22.404L37.417,21.188L36.354,30.119Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-82,0)">
<g id="Artboard1" transform="matrix(1,0,0,1,82,0)">
<rect x="0" y="0" width="64" height="64" style="fill:none;"/>
<clipPath id="_clip1">
<rect x="0" y="0" width="64" height="64"/>
</clipPath>
<g clip-path="url(#_clip1)">
<g transform="matrix(1,0,0,1,-82,0)">
<path d="M114.235,58.85L101,58.85C93.825,58.85 88,53.025 88,45.85L88,18.367C88,11.192 93.825,5.367 101,5.367L127,5.367C134.175,5.367 140,11.192 140,18.367L140,31.684C138.344,30.666 136.522,29.892 134.583,29.414L134.583,18.367C134.583,14.182 131.185,10.784 127,10.784L101,10.784C96.815,10.784 93.417,14.182 93.417,18.367L93.417,45.85C93.417,50.036 96.815,53.434 101,53.434L111.638,53.434C112.218,55.387 113.102,57.211 114.235,58.85Z"/>
</g>
<g transform="matrix(0.121769,0,0,0.121769,14.308464,17.834503)">
<path d="M8.945,187.103L8.945,45C8.945,41.153 10.395,37.687 13.54,34.715C16.57,31.856 19.983,30.533 23.684,30.533L101.541,30.533C105.244,30.533 108.657,31.856 111.685,34.716C114.831,37.688 116.28,41.153 116.28,45L116.28,63.239C116.28,67.084 114.831,70.551 111.685,73.523C108.657,76.383 105.244,77.706 101.541,77.706L59.112,77.706L59.112,99.952L95.825,99.952C99.527,99.952 102.939,101.273 105.967,104.134C109.115,107.105 110.563,110.572 110.563,114.418L110.563,132.658C110.563,136.503 109.116,139.968 105.968,142.941C102.939,145.803 99.527,147.124 95.825,147.124L59.112,147.123L59.112,187.103C59.112,190.807 57.79,194.219 54.928,197.248C51.956,200.394 48.491,201.842 44.646,201.842L23.412,201.842C19.565,201.842 16.1,200.393 13.128,197.247C10.268,194.219 8.946,190.807 8.945,187.103Z"/>
</g>
<g transform="matrix(0.121769,0,0,0.121769,-40.771914,17.834503)">
<path d="M591.369,171.626C580.987,156.298 575.724,137.883 575.724,116.331C575.724,91.129 582.921,70.216 597.085,53.512C611.534,36.472 631.157,27.818 656.07,27.818C677.329,27.818 694.356,32.953 707.26,42.842C720.259,52.803 729.209,67.651 733.791,87.574C734,88.48 734.021,89.418 733.855,90.333C733.839,90.422 733.822,90.511 733.804,90.6C732.171,90.549 730.531,90.524 728.886,90.524C713.323,90.524 698.283,92.793 684.08,97.019C683.789,96.57 683.51,96.096 683.244,95.596C683,95.139 682.809,94.657 682.674,94.157C680.956,87.783 678.047,82.918 673.679,79.732C669.187,76.455 663.286,74.99 656.07,74.99C646.542,74.99 639.4,78.26 634.849,85.227C629.672,93.155 627.252,103.55 627.252,116.331C627.252,120.193 627.479,123.842 627.944,127.277C613.205,139.652 600.767,154.681 591.369,171.626Z"/>
</g>
<g transform="matrix(0.485113,0,0,0.485113,32,32.108844)">
<path d="M66.016,33C66.016,51.203 51.234,66 33,66C14.797,66 0,51.203 0,33C0,14.781 14.797,0 33,0C51.234,0 66.016,14.781 66.016,33ZM27.094,19.5C24.859,19.5 23.438,20.75 23.438,22.734C23.438,24.734 24.859,25.969 27.078,25.969L31.812,25.969L35.847,25.339L31.234,29.375L20.781,39.828C20.031,40.562 19.609,41.578 19.609,42.547C19.609,44.75 21.234,46.219 23.281,46.219C24.375,46.219 25.312,45.844 26.156,45.016L36.516,34.656L40.507,30.084L39.969,34.312L39.969,38.75C39.969,40.953 41.219,42.391 43.234,42.391C45.188,42.391 46.438,40.922 46.438,38.75L46.438,23.797C46.438,20.984 44.812,19.5 42.094,19.5L27.094,19.5Z" style="fill-rule:nonzero;"/>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<rect id="Artboard1" x="0" y="0" width="64" height="64" style="fill:none;"/>
<g id="Artboard11" serif:id="Artboard1">
<g transform="matrix(0.787693,0,0,0.787693,6,6.006144)">
<path d="M32.595,65.998C14.563,65.781 0,51.084 0,33C0,14.781 14.781,0 33,0C51.1,0 65.797,14.564 66.013,32.597C64.003,31.361 61.8,30.409 59.459,29.798C58.946,25.474 57.39,21.467 55.038,18.044C53.252,19.386 50.843,20.54 47.998,21.441C48.59,23.904 48.996,26.56 49.179,29.359C44.485,30.163 40.247,32.317 36.882,35.406L35.437,35.406L35.437,36.848C32.698,39.819 30.69,43.476 29.7,47.53C22.786,47.922 16.949,49.55 13.541,52.117C13.713,51.988 13.892,51.861 14.082,51.738C17.087,54.794 20.831,57.118 25.018,58.434C22.762,56.224 20.842,53.084 19.416,49.278C20.904,48.804 22.542,48.417 24.297,48.124C25.593,51.493 27.274,54.199 29.137,55.808C29.348,57.858 29.815,59.833 30.505,61.701C28.928,61.293 27.428,60.491 26.037,59.349C27.43,60.505 28.934,61.317 30.515,61.729C31.07,63.227 31.769,64.655 32.595,65.998ZM6.474,35.406C6.886,40.076 8.511,44.4 11.044,48.053C12.829,46.735 15.216,45.6 18.025,44.712C17.325,41.847 16.882,38.72 16.753,35.406L6.474,35.406ZM52.484,14.02C52.335,14.133 52.181,14.244 52.014,14.35C48.978,11.231 45.176,8.866 40.916,7.541C43.22,9.772 45.175,12.973 46.615,16.866C45.134,17.339 43.502,17.726 41.755,18.022C40.131,13.743 37.888,10.527 35.437,9.228L35.437,18.669C42.758,18.365 48.953,16.691 52.484,14.02ZM30.697,4.271C28.855,4.713 27.116,5.696 25.532,7.13C27.115,5.709 28.851,4.737 30.688,4.299L30.697,4.271ZM21.697,30.562L30.594,30.562L30.594,23.513C27.88,23.402 25.274,23.103 22.853,22.645C22.247,25.113 21.847,27.793 21.697,30.562ZM40.497,7.15C38.915,5.712 37.177,4.726 35.335,4.279L35.343,4.307C37.181,4.749 38.916,5.726 40.497,7.15ZM10.972,18.051C8.486,21.673 6.889,25.948 6.478,30.562L16.757,30.562C16.893,27.318 17.327,24.253 18.005,21.438C15.168,20.539 12.761,19.388 10.972,18.051ZM25.091,7.542C20.832,8.867 17.03,11.234 13.995,14.355C13.823,14.245 13.663,14.131 13.509,14.015C17.058,16.692 23.268,18.369 30.594,18.67L30.594,9.212C28.138,10.505 25.887,13.729 24.26,18.022C22.509,17.726 20.874,17.338 19.389,16.863C20.831,12.972 22.788,9.772 25.091,7.542ZM22.875,43.507C25.29,43.051 27.889,42.754 30.594,42.643L30.594,35.406L21.687,35.406C21.835,38.238 22.247,40.982 22.875,43.507ZM43.162,22.644C40.747,23.101 38.146,23.399 35.437,23.512L35.437,30.562L44.319,30.562C44.169,27.793 43.769,25.113 43.162,22.644Z"/>
</g>
<g transform="matrix(0.485113,0,0,0.485113,31.974958,31.982526)">
<path d="M66.016,33C66.016,51.203 51.234,66 33,66C14.797,66 0,51.203 0,33C0,14.781 14.797,0 33,0C51.234,0 66.016,14.781 66.016,33ZM27.094,19.5C24.859,19.5 23.438,20.75 23.438,22.734C23.438,24.734 24.859,25.969 27.078,25.969L31.812,25.969L35.847,25.339L31.234,29.375L20.781,39.828C20.031,40.562 19.609,41.578 19.609,42.547C19.609,44.75 21.234,46.219 23.281,46.219C24.375,46.219 25.312,45.844 26.156,45.016L36.516,34.656L40.507,30.084L39.969,34.312L39.969,38.75C39.969,40.953 41.219,42.391 43.234,42.391C45.188,42.391 46.438,40.922 46.438,38.75L46.438,23.797C46.438,20.984 44.812,19.5 42.094,19.5L27.094,19.5Z" style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-0.5,-4)">
<path d="M59.063,37.932C60.996,39.115 62.286,41.246 62.286,43.675L62.286,59.025C62.286,62.739 59.271,65.754 55.558,65.754L9.442,65.754C5.729,65.754 2.714,62.739 2.714,59.025L2.714,43.675C2.714,41.692 3.574,39.908 4.941,38.676L4.941,23.857C4.941,21.49 6.144,19.403 8.186,18.218L28.762,6.357C30.813,5.136 33.19,5.136 35.263,6.357L55.817,18.218C57.859,19.403 59.063,21.49 59.063,23.857L59.063,37.932ZM10.076,36.947L27.371,36.947L10.076,27.093L10.076,36.947ZM36.574,36.947L53.923,36.947L53.923,27.065L36.574,36.947ZM31.984,33.577L52.082,22.106C51.964,21.992 51.921,21.927 51.713,21.8L33.492,11.283C32.55,10.719 31.453,10.719 30.511,11.283L12.312,21.8C12.083,21.927 12.03,21.997 11.892,22.125L31.984,33.577ZM58.286,43.675C58.286,42.169 57.064,40.947 55.558,40.947L9.442,40.947C7.936,40.947 6.714,42.169 6.714,43.675L6.714,59.025C6.714,60.531 7.936,61.754 9.442,61.754L55.558,61.754C57.064,61.754 58.286,60.531 58.286,59.025L58.286,43.675Z"/>
<g transform="matrix(1.711502,0,0,1.711502,-43.419476,-29.526577)">
<path d="M34.865,52.259C37.413,52.259 38.935,51.026 38.935,49.082C38.935,47.56 37.996,46.722 35.931,46.326L34.939,46.139C33.887,45.938 33.451,45.656 33.451,45.147C33.451,44.577 33.974,44.174 34.865,44.174C35.576,44.174 36.112,44.409 36.428,45.012C36.716,45.482 37.051,45.676 37.587,45.676C38.204,45.669 38.62,45.287 38.62,44.717C38.62,44.516 38.586,44.355 38.519,44.188C38.05,42.961 36.696,42.25 34.845,42.25C32.62,42.25 31.004,43.45 31.004,45.314C31.004,46.789 32.01,47.734 33.934,48.096L34.933,48.284C36.079,48.505 36.488,48.78 36.488,49.316C36.488,49.906 35.864,50.335 34.906,50.335C34.128,50.335 33.478,50.081 33.149,49.477C32.834,48.995 32.512,48.827 32.036,48.827C31.413,48.827 30.97,49.243 30.97,49.859C30.97,50.061 31.011,50.268 31.098,50.469C31.5,51.468 32.767,52.259 34.865,52.259Z" style="fill-rule:nonzero;"/>
<path d="M41.308,52.239C42.086,52.239 42.535,51.777 42.535,50.959L42.535,49.357L43.433,48.418L45.941,51.549C46.336,52.052 46.691,52.246 47.208,52.246C47.878,52.246 48.388,51.73 48.388,51.059C48.388,50.718 48.22,50.349 47.818,49.853L45.344,46.823L47.657,44.382C47.985,44.027 48.113,43.759 48.113,43.397C48.113,42.767 47.596,42.264 46.953,42.264C46.537,42.264 46.229,42.425 45.88,42.814L42.589,46.46L42.535,46.46L42.535,43.551C42.535,42.733 42.086,42.27 41.308,42.27C40.53,42.27 40.075,42.733 40.075,43.551L40.075,50.959C40.075,51.777 40.53,52.239 41.308,52.239Z" style="fill-rule:nonzero;"/>
<path d="M53.583,52.259C56.097,52.259 57.746,50.832 57.746,48.659L57.746,43.551C57.746,42.733 57.297,42.27 56.513,42.27C55.735,42.27 55.286,42.733 55.286,43.551L55.286,48.398C55.286,49.524 54.676,50.195 53.583,50.195C52.484,50.195 51.874,49.524 51.874,48.398L51.874,43.551C51.874,42.733 51.424,42.27 50.647,42.27C49.869,42.27 49.413,42.733 49.413,43.551L49.413,48.659C49.413,50.832 51.062,52.259 53.583,52.259Z" style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.983114,0,0,0.983114,0.872652,-3.62694)">
<path d="M29.119,60.411L19.595,65.918C18.255,66.699 16.701,66.699 15.368,65.918L3.47,59.042C2.153,58.292 1.364,56.929 1.364,55.39L1.364,41.653C1.364,40.114 2.153,38.751 3.47,37.993L15.368,31.125C15.408,31.101 15.448,31.079 15.488,31.057C15.484,30.981 15.482,30.906 15.482,30.83L15.482,17.094C15.482,15.554 16.271,14.192 17.588,13.433L29.487,6.565C30.819,5.777 32.374,5.777 33.714,6.565L45.605,13.433C46.922,14.192 47.71,15.554 47.71,17.094L47.71,30.83C47.71,30.882 47.71,30.934 47.708,30.986C47.794,31.029 47.879,31.075 47.963,31.125L59.854,37.993C60.373,38.292 60.811,38.685 61.15,39.146C60.959,39.134 60.762,39.128 60.56,39.128L54.077,39.128L46.646,34.839C46.14,34.532 45.551,34.532 45.045,34.839L37.619,39.128L35.314,39.128C33.245,39.128 31.768,39.759 30.759,40.772C29.905,41.629 29.317,42.828 29.16,44.457L19.411,50.038L19.411,61.546C19.603,61.446 19.687,61.393 19.886,61.278L28.929,56.048C29.434,55.75 30.678,54.945 30.755,55.126L30.857,55.226L30.757,55.324C29.753,56.334 29.119,57.815 29.119,59.896L29.119,60.411ZM33.53,36.986C33.721,36.887 33.805,36.833 34.005,36.718L43.047,31.489C43.553,31.19 43.844,30.685 43.844,30.103L43.844,19.574L33.53,25.478L33.53,36.986ZM29.663,36.94L29.663,25.531L19.341,19.62L19.341,30.103C19.341,30.57 19.539,30.987 19.878,31.288L29.663,36.94ZM31.554,22.132L42.251,15.999C42.197,15.953 42.182,15.93 42.067,15.861L32.397,10.279C31.891,9.973 31.302,9.973 30.796,10.279L21.133,15.861C21.011,15.93 20.98,15.96 20.896,16.029L31.554,22.132ZM17.928,34.679C17.513,34.548 17.07,34.601 16.678,34.839L7.015,40.42C6.892,40.489 6.862,40.52 6.777,40.589L17.436,46.692L28.117,40.567L17.928,34.679ZM15.545,61.546L15.545,50.091L5.223,44.18L5.223,54.662C5.223,55.244 5.529,55.75 6.035,56.048L15.261,61.385C15.391,61.462 15.414,61.477 15.545,61.546Z"/>
</g>
<g transform="matrix(0.446187,0,0,0.446187,32,37.312437)">
<path d="M46.094,59.812L63.672,59.812C69.125,59.812 71.719,57.234 71.719,51.688L71.719,40.219C71.719,34.703 69.125,32.109 63.672,32.109L46.094,32.109C40.641,32.109 38.047,34.703 38.047,40.219L38.047,51.688C38.047,57.234 40.641,59.812 46.094,59.812ZM46.109,53.375C44.984,53.375 44.484,52.844 44.484,51.734L44.484,40.203C44.484,39.078 44.984,38.547 46.109,38.547L63.672,38.547C64.766,38.547 65.281,39.078 65.281,40.203L65.281,51.734C65.281,52.844 64.766,53.375 63.672,53.375L46.109,53.375Z" style="fill-rule:nonzero;"/>
<path d="M8.047,59.812L25.625,59.812C31.078,59.812 33.672,57.234 33.672,51.688L33.672,40.219C33.672,34.703 31.078,32.109 25.625,32.109L8.047,32.109C2.594,32.109 0,34.703 0,40.219L0,51.688C0,57.234 2.594,59.812 8.047,59.812ZM8.047,53.375C6.938,53.375 6.438,52.844 6.438,51.734L6.438,40.203C6.438,39.078 6.938,38.547 8.047,38.547L25.609,38.547C26.703,38.547 27.234,39.078 27.234,40.203L27.234,51.734C27.234,52.844 26.703,53.375 25.609,53.375L8.047,53.375Z" style="fill-rule:nonzero;"/>
<path d="M46.094,27.75L63.672,27.75C69.125,27.75 71.719,25.156 71.719,19.625L71.719,8.172C71.719,2.625 69.125,0.062 63.672,0.062L46.094,0.062C40.641,0.062 38.047,2.625 38.047,8.172L38.047,19.625C38.047,25.156 40.641,27.75 46.094,27.75ZM46.109,21.312C44.984,21.312 44.484,20.797 44.484,19.656L44.484,8.125C44.484,7.016 44.984,6.5 46.109,6.5L63.672,6.5C64.766,6.5 65.281,7.016 65.281,8.125L65.281,19.656C65.281,20.797 64.766,21.312 63.672,21.312L46.109,21.312Z" style="fill-rule:nonzero;"/>
<path d="M8.047,27.75L25.625,27.75C31.078,27.75 33.672,25.156 33.672,19.625L33.672,8.172C33.672,2.625 31.078,0.062 25.625,0.062L8.047,0.062C2.594,0.062 0,2.625 0,8.172L0,19.625C0,25.156 2.594,27.75 8.047,27.75ZM8.047,21.312C6.938,21.312 6.438,20.797 6.438,19.656L6.438,8.125C6.438,7.016 6.938,6.5 8.047,6.5L25.609,6.5C26.703,6.5 27.234,7.016 27.234,8.125L27.234,19.656C27.234,20.797 26.703,21.312 25.609,21.312L8.047,21.312Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0.446187,0,0,0.446187,32,37.312437)">
<path d="M46.094,59.812L63.672,59.812C69.125,59.812 71.719,57.234 71.719,51.688L71.719,40.219C71.719,34.703 69.125,32.109 63.672,32.109L46.094,32.109C40.641,32.109 38.047,34.703 38.047,40.219L38.047,51.688C38.047,57.234 40.641,59.812 46.094,59.812ZM46.109,53.375C44.984,53.375 44.484,52.844 44.484,51.734L44.484,40.203C44.484,39.078 44.984,38.547 46.109,38.547L63.672,38.547C64.766,38.547 65.281,39.078 65.281,40.203L65.281,51.734C65.281,52.844 64.766,53.375 63.672,53.375L46.109,53.375Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.983114,0,0,0.983114,0.872652,-3.62694)">
<path d="M1.364,59.66L1.364,41.653C1.364,40.114 2.153,38.751 3.47,37.993L15.368,31.125C15.408,31.101 15.448,31.079 15.488,31.057C15.484,30.981 15.482,30.906 15.482,30.83L15.482,17.094C15.482,15.554 16.271,14.192 17.588,13.433L29.487,6.565C30.819,5.777 32.374,5.777 33.714,6.565L45.605,13.433C46.922,14.192 47.71,15.554 47.71,17.094L47.71,30.83C47.71,30.882 47.71,30.934 47.708,30.986C47.794,31.029 47.879,31.075 47.963,31.125L59.854,37.993C61.171,38.751 61.96,40.114 61.96,41.653L61.96,59.66C61.96,63.437 58.893,66.504 55.116,66.504L8.208,66.504C4.431,66.504 1.364,63.437 1.364,59.66ZM50.74,37.202L46.646,34.839C46.14,34.532 45.551,34.532 45.045,34.839L40.954,37.202L50.74,37.202ZM22.294,37.202L17.928,34.679C17.513,34.548 17.07,34.601 16.678,34.839L12.587,37.202L22.294,37.202ZM31.554,22.132L42.251,15.999C42.197,15.953 42.182,15.93 42.067,15.861L32.397,10.279C31.891,9.973 31.302,9.973 30.796,10.279L21.133,15.861C21.011,15.93 20.98,15.96 20.896,16.029L31.554,22.132ZM29.663,36.94L29.663,25.531L19.341,19.62L19.341,30.103C19.341,30.57 19.539,30.987 19.878,31.288L29.663,36.94ZM33.53,36.986C33.721,36.887 33.805,36.833 34.005,36.718L43.047,31.489C43.553,31.19 43.844,30.685 43.844,30.103L43.844,19.574L33.53,25.478L33.53,36.986ZM57.891,44.046C57.891,42.514 56.648,41.271 55.116,41.271L8.208,41.271C6.676,41.271 5.433,42.514 5.433,44.046L5.433,59.66C5.433,61.191 6.676,62.435 8.208,62.435L55.116,62.435C56.648,62.435 57.891,61.191 57.891,59.66L57.891,44.046Z"/>
<g transform="matrix(1.740899,0,0,1.740899,-45.561479,-30.413194)">
<path d="M34.865,52.259C37.413,52.259 38.935,51.026 38.935,49.082C38.935,47.56 37.996,46.722 35.931,46.326L34.939,46.139C33.887,45.938 33.451,45.656 33.451,45.147C33.451,44.577 33.974,44.174 34.865,44.174C35.576,44.174 36.112,44.409 36.428,45.012C36.716,45.482 37.051,45.676 37.587,45.676C38.204,45.669 38.62,45.287 38.62,44.717C38.62,44.516 38.586,44.355 38.519,44.188C38.05,42.961 36.696,42.25 34.845,42.25C32.62,42.25 31.004,43.45 31.004,45.314C31.004,46.789 32.01,47.734 33.934,48.096L34.933,48.284C36.079,48.505 36.488,48.78 36.488,49.316C36.488,49.906 35.864,50.335 34.906,50.335C34.128,50.335 33.478,50.081 33.149,49.477C32.834,48.995 32.512,48.827 32.036,48.827C31.413,48.827 30.97,49.243 30.97,49.859C30.97,50.061 31.011,50.268 31.098,50.469C31.5,51.468 32.767,52.259 34.865,52.259Z" style="fill-rule:nonzero;"/>
<path d="M41.308,52.239C42.086,52.239 42.535,51.777 42.535,50.959L42.535,49.357L43.433,48.418L45.941,51.549C46.336,52.052 46.691,52.246 47.208,52.246C47.878,52.246 48.388,51.73 48.388,51.059C48.388,50.718 48.22,50.349 47.818,49.853L45.344,46.823L47.657,44.382C47.985,44.027 48.113,43.759 48.113,43.397C48.113,42.767 47.596,42.264 46.953,42.264C46.537,42.264 46.229,42.425 45.88,42.814L42.589,46.46L42.535,46.46L42.535,43.551C42.535,42.733 42.086,42.27 41.308,42.27C40.53,42.27 40.075,42.733 40.075,43.551L40.075,50.959C40.075,51.777 40.53,52.239 41.308,52.239Z" style="fill-rule:nonzero;"/>
<path d="M53.583,52.259C56.097,52.259 57.746,50.832 57.746,48.659L57.746,43.551C57.746,42.733 57.297,42.27 56.513,42.27C55.735,42.27 55.286,42.733 55.286,43.551L55.286,48.398C55.286,49.524 54.676,50.195 53.583,50.195C52.484,50.195 51.874,49.524 51.874,48.398L51.874,43.551C51.874,42.733 51.424,42.27 50.647,42.27C49.869,42.27 49.413,42.733 49.413,43.551L49.413,48.659C49.413,50.832 51.062,52.259 53.583,52.259Z" style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.069972,0,0,0.069972,-2.986098,-11.802594)">
<path d="M242,226C206,232 174.833,248.333 148.5,275C122.167,301.667 106,333 100,369L100,884C106,919.333 122,950 148,976C174,1002 204.333,1018.333 239,1025L243,1026L758,1026L761,1025C796.333,1018.333 826.833,1001.833 852.5,975.5C878.167,949.167 894,918.333 900,883L900,368C894,332.667 877.667,301.667 851,275C824.333,248.333 793,232 757,226L242,226Z" style="fill:rgb(204,93,21);fill-rule:nonzero;"/>
<g transform="matrix(1,0,0,1,0.001401,0)">
<g transform="matrix(3.343963,0,0,3.343963,60.268889,206.334409)">
<circle cx="68" cy="189" r="24" style="fill:white;"/>
</g>
<g transform="matrix(3.343963,0,0,3.343963,60.268889,206.334409)">
<path d="M160,213L126,213C126,168.016 88.984,131 44,131L44,97C107.636,97 160,149.364 160,213Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(3.343963,0,0,3.343963,60.268889,206.334409)">
<path d="M184,213C184,136.198 120.802,73 44,73L44,38C140.002,38 219,116.998 219,213L184,213Z" style="fill:white;fill-rule:nonzero;"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M43.002,56.36L34.763,61.13C32.69,62.329 30.313,62.329 28.262,61.13L7.686,49.23C5.644,48.067 4.441,45.98 4.441,43.613L4.441,19.857C4.441,17.49 5.644,15.403 7.686,14.218L28.262,2.357C30.313,1.136 32.69,1.136 34.763,2.357L55.317,14.218C57.359,15.403 58.563,17.49 58.563,19.857L58.563,29.046C58.328,29.131 58.089,29.228 57.849,29.338L53.423,31.365L53.423,23.065L34.072,34.087L34.072,40.228L32.231,41.071L32.221,41.076C30.543,41.84 29.474,43.061 28.932,44.416L28.932,34.121L9.576,23.093L9.576,42.668C9.576,43.743 10.14,44.685 11.082,45.245L28.484,55.304C28.7,55.436 28.732,55.463 28.932,55.556L28.932,48.876L29.226,49.54L29.779,50.37L30.487,51.108C31.389,51.893 32.586,52.451 34.072,52.537L34.072,55.556C34.36,55.41 34.43,55.359 34.74,55.186L39.287,52.558L42.996,52.566L43.002,56.36ZM19.147,13.561L11.812,17.8C11.583,17.927 11.53,17.997 11.392,18.125L31.484,29.577L38.568,25.534C34.008,22.886 22.086,15.408 19.147,13.561ZM43.622,22.649L51.582,18.106C51.464,17.992 51.421,17.927 51.213,17.8L32.992,7.283C32.05,6.719 30.953,6.719 30.011,7.283L24.048,10.729L43.622,22.649Z"/>
<g transform="matrix(0.512064,0,0,0.512064,31.502,31.239544)">
<path d="M3.845,24.535C-2.218,27.285 -0.765,35.739 5.782,35.754L28.016,35.801C28.235,35.801 28.298,35.879 28.298,36.098L28.329,58.239C28.345,64.864 36.86,66.16 39.673,59.973L62.563,10.082C65.594,3.411 60.548,-1.433 53.891,1.614L3.845,24.535ZM15.626,28.02C15.454,28.02 15.407,27.895 15.61,27.801L53.079,10.754C53.313,10.661 53.469,10.707 53.329,11.02L36.219,48.457C36.157,48.614 36.032,48.567 36.032,48.41L36.141,31.973C36.157,29.067 35.001,27.895 32.079,27.91L15.626,28.02Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M37.609,59.482L34.763,61.13C32.69,62.329 30.313,62.329 28.262,61.13L7.686,49.23C5.644,48.067 4.441,45.98 4.441,43.613L4.441,19.857C4.441,17.49 5.644,15.403 7.686,14.218L28.262,2.357C30.313,1.136 32.69,1.136 34.763,2.357L55.317,14.218C57.359,15.403 58.563,17.49 58.563,19.857L58.563,29.069L53.423,29.069L53.423,23.065L48.562,25.834L48.238,24.714L47.694,23.67L46.948,22.757L46.033,22.016L45.35,21.663L51.582,18.106C51.464,17.992 51.421,17.927 51.213,17.8L32.992,7.283C32.05,6.719 30.953,6.719 30.011,7.283L24.048,10.729L41.312,21.242L40.408,21.525L39.397,22.064L38.468,22.819L36.769,24.469C31.434,21.272 21.754,15.2 19.147,13.561L11.812,17.8C11.583,17.927 11.53,17.997 11.392,18.125L31.484,29.577L31.548,29.541L30.36,30.694C30.348,30.712 29.57,31.624 29.57,31.624L28.978,32.699L28.621,33.855L28.613,33.939L9.576,23.093L9.576,42.668C9.576,43.743 10.14,44.685 11.082,45.245L28.484,55.304C28.672,55.419 28.72,55.454 28.861,55.522L28.943,55.802L29.486,56.835L30.221,57.739L31.121,58.481L32.155,59.031L33.284,59.369L34.463,59.482L37.609,59.482ZM34.072,42.976L34.072,47.577L33.291,47.651L32.166,47.984L31.133,48.529L30.231,49.266L29.492,50.169L28.946,51.202L28.932,51.249L28.932,37.203L28.987,37.379L29.571,38.438L30.361,39.372L34.072,42.976Z"/>
<g transform="matrix(0.504578,0,0,0.504578,32,24.523529)">
<path d="M53.106,52.746L42.032,52.551L4.882,52.551C2.095,52.551 0,54.639 0,57.433C0,60.236 2.095,62.347 4.882,62.347L42.032,62.347L53.106,62.113C56.566,62.084 58.318,59.77 58.318,57.426C58.318,55.106 56.566,52.769 53.106,52.746ZM38.717,69.984C37.78,70.874 37.339,72.072 37.339,73.386C37.339,76.204 39.34,78.237 42.19,78.237C43.434,78.237 44.82,77.654 45.709,76.765L61.858,61.062C63.936,59.039 63.944,55.842 61.858,53.836L45.709,38.133C44.82,37.244 43.434,36.661 42.19,36.661C39.34,36.661 37.339,38.694 37.339,41.512C37.339,42.826 37.78,44 38.717,44.906L47.179,53.013L52.155,57.433L47.036,62.028L38.717,69.984Z" style="fill-rule:nonzero;"/>
<path d="M10.303,16.133C6.843,16.155 5.114,18.475 5.114,20.82C5.114,23.14 6.843,25.477 10.303,25.507L21.408,25.717L58.527,25.717C61.321,25.717 63.409,23.629 63.409,20.827C63.409,18.032 61.321,15.945 58.527,15.945L21.408,15.945L10.303,16.133ZM24.691,33.354L16.373,25.422L11.261,20.827L16.23,16.383L24.691,8.3C25.652,7.387 26.101,6.22 26.101,4.906C26.101,2.063 24.069,0.055 21.242,0.055C19.982,0.055 18.62,0.63 17.724,1.527L1.582,17.205C-0.535,19.236 -0.496,22.433 1.582,24.449L17.724,40.127C18.62,41.024 19.982,41.606 21.242,41.606C24.069,41.606 26.101,39.591 26.101,36.748C26.101,35.442 25.652,34.244 24.691,33.354Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.774546,0,0,0.774546,1,13.356447)">
<path d="M42.516,48.141C45.406,48.141 46.984,46.891 47.812,43.844L58.703,9.766L58.906,9.766L69.609,43.844C70.438,46.891 72,48.141 74.891,48.141C78.016,48.141 80.047,46.266 80.047,43.375C80.047,42.234 79.891,41.312 79.484,40.141L67.109,5.75C65.766,1.859 63.125,0 58.781,0C54.688,0 52.062,1.859 50.719,5.734L38.141,40.531C37.766,41.594 37.594,42.594 37.594,43.5C37.594,46.375 39.469,48.141 42.516,48.141ZM47.891,36.469L69.562,36.469C71.672,36.469 73.391,34.75 73.391,32.641C73.391,30.516 71.672,28.812 69.562,28.812L47.891,28.812C45.766,28.812 44.062,30.516 44.062,32.641C44.062,34.75 45.766,36.469 47.891,36.469Z" style="fill-rule:nonzero;"/>
<path d="M4.25,48.141C6.453,48.141 7.766,46.984 8.531,44.219L10.141,39.25L22.188,39.25L23.781,44.312C24.5,47.047 25.812,48.141 28.188,48.141C30.641,48.141 32.391,46.5 32.391,44.109C32.391,43.156 32.188,42.297 31.797,41.203L22.781,16.75C21.531,13.328 19.469,11.766 16.078,11.766C12.875,11.766 10.812,13.344 9.594,16.75L0.609,41.203C0.234,42.203 0,43.281 0,44.109C0,46.625 1.609,48.141 4.25,48.141ZM11.875,32.938L15.797,19.891L16.328,19.891L20.344,32.938L11.875,32.938Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.862654,0,0,0.862654,3,7.576119)">
<rect x="0" y="0" width="67.234" height="56.625" style="fill-opacity:0;"/>
<path d="M3.656,56.578L63.359,56.578C65.406,56.578 67.047,54.844 67.047,52.828C67.047,50.75 65.422,49.062 63.359,49.062L3.656,49.062C1.609,49.062 0,50.797 0,52.828C0,54.812 1.625,56.578 3.656,56.578Z" style="fill-rule:nonzero;"/>
<path d="M3.656,40.234L63.359,40.234C65.406,40.234 67.047,38.5 67.047,36.484C67.047,34.391 65.422,32.719 63.359,32.719L3.656,32.719C1.609,32.719 0,34.422 0,36.484C0,38.453 1.625,40.234 3.656,40.234Z" style="fill-rule:nonzero;"/>
<path d="M3.656,23.875L63.359,23.875C65.406,23.875 67.047,22.125 67.047,20.109C67.047,18.047 65.422,16.359 63.359,16.359L3.656,16.359C1.609,16.359 0,18.078 0,20.109C0,22.094 1.625,23.875 3.656,23.875Z" style="fill-rule:nonzero;"/>
<path d="M3.656,7.516L63.359,7.516C65.406,7.516 67.047,5.781 67.047,3.766C67.047,1.688 65.422,0 63.359,0L3.656,0C1.609,0 0,1.734 0,3.766C0,5.75 1.625,7.516 3.656,7.516Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,12.21875,-7.71875)">
<path d="M2.859,68.875L36.688,68.875C38.328,68.875 39.562,67.719 39.562,66.109C39.562,64.516 38.328,63.328 36.688,63.328L2.859,63.328C1.234,63.328 0,64.516 0,66.109C0,67.719 1.234,68.875 2.859,68.875Z" style="fill-rule:nonzero;"/>
<path d="M19.781,58.359C31.812,58.359 39.562,51.5 39.562,41.203L39.562,15.281C39.562,12.328 37.672,10.562 34.703,10.562C31.766,10.562 29.875,12.328 29.875,15.281L29.875,40.234C29.875,46.328 26.203,50.156 19.781,50.156C13.359,50.156 9.703,46.328 9.703,40.234L9.703,15.281C9.703,12.328 7.781,10.562 4.828,10.562C1.906,10.562 0.016,12.328 0.016,15.281L0.016,41.203C0.016,51.5 7.75,58.359 19.781,58.359Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.773655,0,0,0.773655,3,3.979177)">
<path d="M6.922,23.938L68.547,23.938C70.812,23.938 72.156,22.234 72.156,20.547C72.156,19.312 71.453,18.188 69.984,17.328L42.109,1.25C40.734,0.453 39.188,0 37.719,0C36.25,0 34.688,0.453 33.344,1.25L5.469,17.328C3.984,18.188 3.297,19.312 3.297,20.547C3.297,22.234 4.641,23.938 6.922,23.938ZM15.047,19.719L36.859,7.5C37.141,7.344 37.469,7.25 37.719,7.25C37.984,7.25 38.297,7.344 38.594,7.5L60.406,19.719L61.016,17.516L14.438,17.516L15.047,19.719ZM8.516,31.609L17.281,31.609C18.797,31.609 19.734,30.75 19.734,29.234L19.734,28.781C19.734,27.25 18.812,26.312 17.281,26.312L8.516,26.312C7,26.312 6.094,27.25 6.094,28.781L6.094,29.234C6.094,30.75 7.031,31.609 8.516,31.609ZM9.891,59.516L15.938,59.516L15.938,30.047L9.891,30.047L9.891,59.516ZM8.516,63.25L17.281,63.25C18.812,63.25 19.734,62.359 19.734,60.812L19.734,60.359C19.734,58.844 18.797,57.953 17.281,57.953L8.516,57.953C7.031,57.953 6.094,58.844 6.094,60.359L6.094,60.812C6.094,62.359 7,63.25 8.516,63.25ZM25.062,31.609L33.828,31.609C35.344,31.609 36.281,30.75 36.281,29.234L36.281,28.781C36.281,27.25 35.359,26.312 33.828,26.312L25.062,26.312C23.547,26.312 22.641,27.25 22.641,28.781L22.641,29.234C22.641,30.75 23.562,31.609 25.062,31.609ZM26.422,59.516L32.469,59.516L32.469,30.047L26.422,30.047L26.422,59.516ZM25.062,63.25L33.828,63.25C35.359,63.25 36.281,62.359 36.281,60.812L36.281,60.359C36.281,58.844 35.344,57.953 33.828,57.953L25.062,57.953C23.562,57.953 22.641,58.844 22.641,60.359L22.641,60.812C22.641,62.359 23.547,63.25 25.062,63.25ZM41.625,31.609L50.375,31.609C51.875,31.609 52.828,30.75 52.828,29.234L52.828,28.781C52.828,27.25 51.891,26.312 50.375,26.312L41.625,26.312C40.078,26.312 39.188,27.25 39.188,28.781L39.188,29.234C39.188,30.75 40.094,31.609 41.625,31.609ZM42.984,59.516L49.031,59.516L49.031,30.047L42.984,30.047L42.984,59.516ZM41.625,63.25L50.375,63.25C51.891,63.25 52.828,62.359 52.828,60.812L52.828,60.359C52.828,58.844 51.875,57.953 50.375,57.953L41.625,57.953C40.094,57.953 39.188,58.844 39.188,60.359L39.188,60.812C39.188,62.359 40.078,63.25 41.625,63.25ZM58.156,31.609L66.922,31.609C68.406,31.609 69.359,30.75 69.359,29.234L69.359,28.781C69.359,27.25 68.422,26.312 66.922,26.312L58.156,26.312C56.625,26.312 55.719,27.25 55.719,28.781L55.719,29.234C55.719,30.75 56.641,31.609 58.156,31.609ZM59.531,59.516L65.578,59.516L65.578,30.047L59.531,30.047L59.531,59.516ZM58.156,63.25L66.922,63.25C68.422,63.25 69.359,62.359 69.359,60.812L69.359,60.359C69.359,58.844 68.406,57.953 66.922,57.953L58.156,57.953C56.641,57.953 55.719,58.844 55.719,60.359L55.719,60.812C55.719,62.359 56.625,63.25 58.156,63.25ZM3.391,72.438L71.547,72.438C73.375,72.438 74.969,70.922 74.969,69.031C74.969,67.188 73.375,65.641 71.547,65.641L3.391,65.641C1.547,65.641 0,67.188 0,69.031C0,70.906 1.547,72.438 3.391,72.438Z" style="fill-rule:nonzero;"/>
</g>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" version="1.1" viewBox="0 0 72 69" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<path d="m20.601 66.402v-19.472h8.932v19.472h28.517c4.163 0 6.597-2.375 6.597-6.48v-23.76c3.033-1.711 5.043-4.945 5.043-8.699v-0.498c0-1.144-0.293-2.17-0.88-3.167l-7.77-13.283v-2.433c0-3.724-2.287-5.952-6.069-5.952h-37.912c-3.783 0-6.099 2.228-6.099 5.952v2.433l-7.77 13.283c-0.587 0.997-0.88 2.023-0.88 3.167v0.498c0 3.754 2.01 6.988 5.043 8.699v23.76c0 4.105 2.434 6.48 6.597 6.48h6.651zm39.325-28.945c-0.098 3e-3 -0.195 5e-3 -0.293 5e-3 -3.255 0-6.099-1.496-7.858-3.871-1.789 2.375-4.604 3.871-7.888 3.871s-6.128-1.496-7.887-3.871c-1.759 2.375-4.603 3.871-7.887 3.871-3.255 0-6.099-1.496-7.858-3.871-1.789 2.375-4.633 3.871-7.888 3.871-0.098 0-0.195-2e-3 -0.293-5e-3v21.262c0 1.906 1.026 2.962 2.903 2.962h1.759v-16.449c0-1.349 0.909-2.229 2.258-2.229h12.432c1.349 0 2.228 0.88 2.228 2.229v16.449h23.399c1.876 0 2.873-1.056 2.873-2.962v-21.262zm-37.443-8.088h11.289c-0.645 2.375-2.639 3.958-5.659 3.958-2.991 0-5.014-1.583-5.63-3.958zm31.52 0h11.289c-0.645 2.375-2.639 3.958-5.659 3.958-2.991 0-5.014-1.583-5.63-3.958zm-47.295 0h11.318c-0.645 2.375-2.639 3.958-5.659 3.958s-5.014-1.583-5.659-3.958zm31.52 0h11.318c-0.645 2.375-2.639 3.958-5.659 3.958s-5.013-1.583-5.659-3.958zm-30.846-4.134 6.363-11.201h44.275l6.539 11.201h-57.177zm8.005-15.629v-0.703c0-1.466 0.851-2.346 2.287-2.346h36.652c1.437 0 2.287 0.88 2.287 2.346v0.703h-41.226z"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -35,10 +35,6 @@
.ant-picker-input,
.ant-picker-header-view button,
.ant-badge,
.ant-select-dropdown,
.ant-splitter,
.ant-steps-item-title,
.ant-steps-icon,
[class*=' ant-radio'] {
font-family: 'DM Sans';
}
@ -48,15 +44,6 @@
font-family: 'DM Mono';
}
.ant-steps .ant-steps-item-icon {
line-height: 30px;
font-weight: 600;
}
.ant-steps .ant-steps-item-icon .anticon.anticon-check.ant-steps-finish-icon {
line-height: 32px;
}
.ant-typography .id-text {
font-family: 'DM Mono';
font-size: 11.9px;
@ -181,6 +168,23 @@ code {
}
}
.markdown-display > .ant-space-item *:first-child {
margin-top: 0;
}
.markdown-display > .ant-space-item *:last-child {
margin-bottom: 0;
}
.markdown-display > .ant-space-item h1,
.markdown-display > .ant-space-item h2,
.markdown-display > .ant-space-item h3,
.markdown-display > .ant-space-item h4,
.markdown-display > .ant-space-item h5,
.markdown-display > .ant-space-item h6 {
margin-bottom: 0.15em;
}
.iddisplay .ant-popover-inner {
padding: 0 !important;
}
@ -413,192 +417,3 @@ body {
.ant-badge .ant-badge-count-sm {
font-size: 10px;
}
.input-number-cal {
position: relative;
}
.input-number-cal .ant-input-suffix {
margin-right: 28px;
}
.input-number-cal .ant-input-outlined {
border-color: var(--color-purple);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-purple) 31%, transparent);
}
.input-number-cal .ant-input {
font-family: 'DM Mono';
font-weight: 400;
}
.input-number-cal-icon {
position: absolute;
right: 10px;
top: 0;
bottom: 1px;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
/* --- Start of src/components/Dashboard/common/MarkdownInput.css --- */
.md-editor__content {
padding: 24px;
font-size: 14px;
line-height: 1.6;
color: var(--baseText);
white-space: pre-wrap;
outline: none;
}
.md-editor {
color: var(--baseText);
}
.md-editor .tiptap {
min-height: var(--md-editor-min-height);
}
.md-editor .tiptap p.is-editor-empty:first-child::before {
content: 'Enter text here...';
color: var(--baseBorderHover);
float: left;
height: 0;
pointer-events: none;
}
.markdown pre,
.markdown code,
.markdown blockquote {
background: var(--baseBgSubtle);
color: var(--baseTextContrast);
}
.markdown pre {
border-radius: 8px;
padding: 12px;
overflow-x: auto;
}
.markdown blockquote {
border-left: 3px solid var(--color-primary);
margin: 0;
padding: 8px 12px;
}
.markdown hr {
border: 0;
border-top: 1px solid #8484844d;
margin: 10px 0;
}
.markdown *:first-child {
margin-top: 0;
}
.markdown h1:first-child {
margin-top: -4px;
}
.markdown h2:first-child {
margin-top: -3px;
}
.markdown h3:first-child {
margin-top: -2px;
}
.markdown h4:first-child {
margin-top: -1px;
}
.markdown *:last-child {
margin-bottom: 0;
}
.markdown h1,
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
margin-bottom: 10px;
line-height: 1;
}
.markdown p {
font-size: 14px;
margin: 10px 0;
line-height: 1;
}
.markdown h1 {
font-size: 32px;
}
.markdown h2 {
font-size: 26px;
}
.markdown h3 {
font-size: 20px;
}
.markdown h4 {
font-size: 16px;
}
.markdown {
width: 100%;
}
.markdown ul,
.markdown ol {
padding-left: 20px;
margin: 10px 0;
}
.markdown li {
margin: 10px 0;
line-height: 1;
}
.markdown-code-editor {
padding: 18px;
}
.markdown-splitter > .ant-splitter-bar {
margin: 8px 0;
}
.markdown-code-editor .cm-editor {
background-color: transparent;
}
.markdown-code-editor .cm-editor.cm-focused {
outline: none;
}
.markdown-code-editor .cm-editor .cm-gutters {
background-color: transparent;
}
.h-100 {
height: 100%;
}
.ant-table.ant-table-middle .ant-table-filter-trigger {
height: 20px;
}
.ant-table-wrapper .ant-table-filter-column {
align-items: center;
}
span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
width: 50px;
min-width: 0;
height: 20px;
}

2906
bun.lock Normal file

File diff suppressed because it is too large Load Diff

4805
dist-test/index.js Normal file

File diff suppressed because it is too large Load Diff

31
electrobun.config.ts Normal file
View File

@ -0,0 +1,31 @@
import type { ElectrobunConfig } from 'electrobun'
import { readFileSync } from 'fs'
const packageJson = JSON.parse(readFileSync('./package.json', 'utf8'))
export default {
app: {
name: 'Farm Control',
identifier: 'com.tombutcher.farmcontrol',
version: packageJson.version,
urlSchemes: ['farmcontrol']
},
runtime: {
exitOnLastWindowClosed: true
},
build: {
bun: {
entrypoint: 'src/bun/index.ts'
},
views: {
preload: {
entrypoint: 'src/preload/index.ts'
}
},
copy: {
'build/index.html': 'views/main/index.html',
'build/assets': 'views/main/assets'
},
watch: ['build']
}
} satisfies ElectrobunConfig

View File

@ -9,6 +9,7 @@
"private": true,
"homepage": "./",
"dependencies": {
"@ant-design/charts": "^2.6.5",
"@ant-design/icons": "^6.1.0",
"@babel/plugin-transform-private-property-in-object": "^7.27.1",
"@codemirror/lang-cpp": "^6.0.3",
@ -28,17 +29,9 @@
"@codemirror/theme-one-dark": "^6.1.3",
"@simplewebauthn/browser": "^13.1.2",
"@tanstack/react-query": "^5.90.10",
"@tiptap/extension-link": "^3.20.1",
"@tiptap/extension-placeholder": "^3.20.1",
"@tiptap/extension-underline": "^3.20.1",
"@tiptap/markdown": "^3.20.1",
"@tiptap/pm": "^3.20.1",
"@tiptap/react": "^3.20.1",
"@tiptap/starter-kit": "^3.20.1",
"@tsparticles/react": "^3.0.0",
"@tsparticles/slim": "^3.9.1",
"@uiw/react-codemirror": "^4.25.1",
"@vscode/sudo-prompt": "^9.3.2",
"antd": "^5.27.1",
"antd-style": "^3.7.1",
"axios": "^1.11.0",
@ -46,13 +39,10 @@
"cross-env": "^10.0.0",
"dayjs": "^1.11.18",
"dotenv": "^17.2.1",
"electron-store": "^11.0.2",
"gcode-preview": "^2.18.0",
"json-schema-traverse": "^1.0.0",
"keycloak-js": "^26.2.0",
"lodash": "^4.17.23",
"loglevel": "^1.9.2",
"nanoid": "^5.1.14",
"online-3d-viewer": "^0.16.0",
"prop-types": "^15.8.1",
"react": "^19.1.1",
@ -61,7 +51,6 @@
"react-markdown": "^10.1.0",
"react-responsive": "^10.0.1",
"react-router-dom": "^7.8.2",
"recharts": "^3.8.1",
"remark-gfm": "^4.0.1",
"simplebar-react": "^3.3.2",
"socket.io-client": "*",
@ -73,15 +62,15 @@
"tsparticles": "^3.9.1",
"web-vitals": "^5.1.0"
},
"main": "build/electron.js",
"main": "src/bun/index.ts",
"description": "3D Printer ERP and Control Software.",
"scripts": {
"dev": "cross-env NODE_ENV=development vite",
"electron": "cross-env ELECTRON_START_URL=http://0.0.0.0:5780 && cross-env NODE_ENV=development && electron .",
"electrobun": "cross-env ELECTROBUN_START_URL=http://localhost:5173 electrobun dev",
"start": "serve -s build",
"build": "vite build",
"dev:electron": "concurrently \"cross-env NODE_ENV=development vite --port 5780 --no-open\" \"cross-env ELECTRON_START_URL=http://localhost:5780 NODE_ENV=development electron public/electron.js\"",
"build:electron": "vite build && electron-builder",
"dev:electrobun": "concurrently \"cross-env NODE_ENV=development vite --no-open\" \"cross-env ELECTROBUN_START_URL=http://localhost:5173 electrobun dev\"",
"build:electrobun": "vite build && electrobun build",
"build:cloudflare": "cross-env VITE_DEPLOY_TARGET=cloudflare vite build",
"deploy": "npm run build:cloudflare && wrangler pages deploy --branch main"
},
@ -107,9 +96,8 @@
"@eslint/js": "^9.39.2",
"@vitejs/plugin-react": "^5.0.2",
"concurrently": "^9.2.1",
"electron": "^38.7.1",
"electron-builder": "^26.0.12",
"electron-packager": "^17.1.2",
"electrobun": "^1.13.1",
"typescript": "^5.0.0",
"eslint": "^9.34.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
@ -134,7 +122,6 @@
"build": {
"appId": "com.tombutcher.farmcontrol",
"productName": "Farm Control",
"artifactName": "farmcontrol-${version}-${arch}.${ext}",
"icon": "assets/logos/farmcontrolicon.png",
"directories": {
"output": "app_dist"
@ -148,25 +135,7 @@
{
"target": "dmg",
"arch": [
"arm64"
]
},
{
"target": "pkg",
"arch": [
"arm64"
]
},
{
"target": "dmg",
"arch": [
"x64"
]
},
{
"target": "pkg",
"arch": [
"x64"
"universal"
]
}
],
@ -184,45 +153,8 @@
]
}
},
"dmg": {
"background": "assets/dmg/background.png",
"iconSize": 100,
"window": {
"width": 540,
"height": 380
},
"contents": [
{
"x": 130,
"y": 220
},
{
"x": 410,
"y": 220,
"type": "link",
"path": "/Applications"
}
]
},
"pkg": {
"installLocation": "/Applications",
"mustClose": [
"com.tombutcher.farmcontrol"
]
},
"win": {
"target": [
"nsis",
"msiWrapped"
],
"protocols": [
{
"name": "Farm Control Protocol",
"schemes": [
"farmcontrol"
]
}
]
"target": "nsis"
},
"linux": {
"target": "AppImage"
@ -232,12 +164,6 @@
"allowToChangeInstallationDirectory": true,
"include": "scripts/installer.nsh",
"perMachine": true
},
"msiWrapped": {
"upgradeCode": "{735812DB-E33B-57A0-8FBC-5FC3155925AA}",
"perMachine": true,
"impersonate": false,
"wrappedInstallerArgs": "/S"
}
}
}

2104
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +0,0 @@
allowBuilds:
electron: true
esbuild: true
sharp: true
workerd: true
core-js: true
electron-winstaller: true
'@tsparticles/engine': true

View File

@ -1,263 +0,0 @@
import { ipcMain } from 'electron'
import { createWriteStream, promises as fs } from 'fs'
import http from 'http'
import https from 'https'
import os from 'os'
import path from 'path'
import process from 'process'
import { launchMacInstaller } from './macappupdate.js'
import { launchWindowsInstaller } from './winappupdate.js'
const UPDATE_PROGRESS_CHANNEL = 'app-update-progress'
const SUPPORTED_TARGETS = {
darwin: {
extension: '.pkg',
osMatchers: ['darwin', 'mac', 'macos', 'osx']
},
win32: {
extension: '.msi',
osMatchers: ['win32', 'win', 'windows']
}
}
let runningUpdate = null
const getArtifactName = (artifact) =>
String(artifact?.fileName || artifact?.relativePath || artifact?.url || '')
const normalizeArch = (arch) => {
if (arch === 'x64' || arch === 'amd64') return 'x64'
if (arch === 'arm64' || arch === 'aarch64') return 'arm64'
return arch
}
const artifactMatchesPlatform = (artifact, target, platform, arch) => {
const name = getArtifactName(artifact).toLowerCase()
const normalizedArch = normalizeArch(arch)
const artifactArch = normalizeArch(String(artifact?.arch || '').toLowerCase())
const artifactPlatform = String(
artifact?.platform || artifact?.os || artifact?.target || ''
).toLowerCase()
if (!name.endsWith(target.extension)) return false
if (!artifact?.url) return false
const matchesArch =
artifactArch === normalizedArch ||
name.includes(`-${normalizedArch}`) ||
name.includes(`_${normalizedArch}`) ||
name.includes(`.${normalizedArch}.`) ||
name.includes(normalizedArch)
const matchesOs =
!artifactPlatform ||
target.osMatchers.includes(artifactPlatform) ||
target.osMatchers.some((matcher) => name.includes(matcher)) ||
(platform === 'darwin' && name.includes('mac')) ||
(platform === 'win32' && name.includes('win'))
return matchesArch && matchesOs
}
const selectUpdateArtifact = (
update,
platform = process.platform,
arch = process.arch
) => {
const target = SUPPORTED_TARGETS[platform]
if (!target) {
throw new Error(`App updates are not supported on ${platform}.`)
}
const artifacts = Array.isArray(update?.artifacts) ? update.artifacts : []
const matchingArtifact = artifacts.find((artifact) =>
artifactMatchesPlatform(artifact, target, platform, arch)
)
const fallbackArtifact = artifacts.find((artifact) => {
const name = getArtifactName(artifact).toLowerCase()
return artifact?.url && name.endsWith(target.extension)
})
if (!matchingArtifact && !fallbackArtifact) {
throw new Error(
`No ${target.extension} update artifact found for ${platform}/${arch}.`
)
}
return matchingArtifact || fallbackArtifact
}
const sendProgress = (webContents, payload) => {
if (!webContents || webContents.isDestroyed()) return
webContents.send(UPDATE_PROGRESS_CHANNEL, {
timestamp: new Date().toISOString(),
...payload
})
}
const getInstallErrorMessage = (error, output = '') => {
const combined = `${output}\n${error?.message || ''}`.trim()
if (
/cancel/i.test(combined) ||
/did not grant permission/i.test(combined) ||
/user canceled/i.test(combined)
) {
return 'Update installation was cancelled.'
}
if (/incorrect/i.test(combined)) {
return 'The administrator password was incorrect.'
}
return combined || 'Failed to install update.'
}
const installerHelpers = { sendProgress, getInstallErrorMessage }
const getDownloadUrl = (url, redirectCount = 0) =>
new Promise((resolve, reject) => {
if (redirectCount > 5) {
reject(new Error('Too many redirects while downloading update.'))
return
}
const parsedUrl = new URL(url)
const client = parsedUrl.protocol === 'https:' ? https : http
const request = client.get(parsedUrl, (response) => {
const location = response.headers.location
if (response.statusCode >= 300 && response.statusCode < 400 && location) {
response.resume()
resolve(
getDownloadUrl(
new URL(location, parsedUrl).toString(),
redirectCount + 1
)
)
return
}
resolve({ response, url: parsedUrl.toString() })
})
request.on('error', reject)
})
const downloadArtifact = async (artifact, destinationPath, webContents) => {
const { response } = await getDownloadUrl(artifact.url)
if (response.statusCode < 200 || response.statusCode >= 300) {
response.resume()
throw new Error(`Update download failed with HTTP ${response.statusCode}.`)
}
const totalBytes =
Number.parseInt(response.headers['content-length'], 10) || 0
let downloadedBytes = 0
await new Promise((resolve, reject) => {
const output = createWriteStream(destinationPath)
response.on('data', (chunk) => {
downloadedBytes += chunk.length
const percent = totalBytes
? Math.round((downloadedBytes / totalBytes) * 100)
: null
sendProgress(webContents, {
phase: 'downloading',
percent,
downloadedBytes,
totalBytes,
message: totalBytes
? `Downloading update (${percent}%)`
: 'Downloading update'
})
})
response.on('error', reject)
output.on('error', reject)
output.on('finish', resolve)
response.pipe(output)
})
}
const restartApp = (app) => {
console.log('[app-update] restarting app')
app.relaunch()
app.exit(0)
}
const launchInstallerAndQuit = async (app, installerPath, webContents) => {
if (process.platform === 'darwin') {
await launchMacInstaller(app, installerPath, webContents, installerHelpers)
restartApp(app)
return
}
if (process.platform === 'win32') {
await launchWindowsInstaller(
app,
installerPath,
webContents,
installerHelpers
)
restartApp(app)
return
}
throw new Error(`App updates are not supported on ${process.platform}.`)
}
const runAppUpdate = async (app, update, webContents) => {
const artifact = selectUpdateArtifact(update)
const tempDirectory = await fs.mkdtemp(
path.join(os.tmpdir(), 'farmcontrol-update-')
)
const artifactName = path.basename(getArtifactName(artifact))
const installerPath = path.join(tempDirectory, artifactName)
sendProgress(webContents, {
phase: 'preparing',
percent: 0,
artifact,
message: 'Preparing update download'
})
await downloadArtifact(artifact, installerPath, webContents)
sendProgress(webContents, {
phase: 'downloaded',
percent: 100,
downloadedBytes: null,
totalBytes: null,
artifact,
message: 'Update downloaded'
})
await launchInstallerAndQuit(app, installerPath, webContents)
}
export function setupAppUpdateIPC(app) {
ipcMain.handle('app-update-start', async (event, update) => {
if (runningUpdate) return runningUpdate
const webContents = event.sender
runningUpdate = runAppUpdate(app, update, webContents)
.then(() => ({ ok: true }))
.catch((error) => {
sendProgress(webContents, {
phase: 'error',
percent: null,
message: error?.message || 'Failed to update app.'
})
throw error
})
.finally(() => {
runningUpdate = null
})
return runningUpdate
})
}

View File

@ -1,7 +1,5 @@
import { app, ipcMain, shell, globalShortcut, safeStorage } from 'electron'
import Store from 'electron-store'
import { Buffer } from 'buffer'
import process from 'process'
import { app, ipcMain, shell, globalShortcut } from 'electron'
import { createRequire } from 'module'
import {
registerGlobalShortcuts,
setupSpotlightIPC
@ -10,85 +8,33 @@ import {
createWindow,
setupMainWindowIPC,
setupMainWindowAppEvents,
setupDevAuthServer,
setupSingleInstanceLock,
handleDeepLinkFromArgv
setupDevAuthServer
} from './mainWindow.js'
import { setupAppUpdateIPC } from './appupdate.js'
// --- Auth session storage (main process) ---
const authStore = new Store({
name: 'auth-session'
})
const AUTH_SESSION_KEY = 'authSession'
const appSettingsStore = new Store({
name: 'settings'
})
const APP_SETTINGS_KEY = 'appSettings'
const serializeAuthSession = (session) => {
const sessionJson = JSON.stringify(session)
if (safeStorage.isEncryptionAvailable()) {
const encrypted = safeStorage.encryptString(sessionJson).toString('base64')
return {
encrypted: true,
value: encrypted
}
}
return {
encrypted: false,
value: sessionJson
}
}
const deserializeAuthSession = (storedValue) => {
if (!storedValue) return null
if (typeof storedValue === 'object' && storedValue.encrypted === true) {
if (!safeStorage.isEncryptionAvailable()) {
// --- Keytar-backed auth session storage (main process) ---
const require = createRequire(import.meta.url)
let keytar = null
try {
// keytar is a native module; in some dev environments it may not be built yet.
keytar = require('keytar')
} catch (e) {
console.warn(
'[auth-session] Encrypted auth session exists but encryption is unavailable on this system.'
'[keytar] Not available; auth session persistence will be disabled.',
e?.message || e
)
return null
}
const decrypted = safeStorage.decryptString(
Buffer.from(storedValue.value, 'base64')
)
return JSON.parse(decrypted)
}
if (typeof storedValue === 'object' && typeof storedValue.value === 'string') {
return JSON.parse(storedValue.value)
}
if (typeof storedValue === 'string') {
return JSON.parse(storedValue)
}
// Legacy safety net if the object shape already matches the session structure.
if (typeof storedValue === 'object' && storedValue.token) {
return storedValue
}
return null
}
const gotTheLock = setupSingleInstanceLock(app)
const KEYTAR_SERVICE = app.name || 'Farm Control'
const KEYTAR_ACCOUNT = 'authSession'
if (gotTheLock) {
app.whenReady().then(() => {
app.whenReady().then(() => {
createWindow()
registerGlobalShortcuts()
setupSpotlightIPC()
setupMainWindowIPC()
setupAppUpdateIPC(app)
setupMainWindowAppEvents(app)
setupDevAuthServer()
handleDeepLinkFromArgv()
})
}
})
app.on('will-quit', () => {
globalShortcut.unregisterAll()
@ -103,52 +49,38 @@ ipcMain.handle('os-info', () => {
ipcMain.handle('auth-session-get', async () => {
try {
const storedValue = authStore.get(AUTH_SESSION_KEY)
return deserializeAuthSession(storedValue)
if (!keytar) return null
const raw = await keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT)
if (!raw) return null
return JSON.parse(raw)
} catch (e) {
console.warn('[auth-session] Failed to read auth session.', e?.message || e)
console.warn('[keytar] Failed to read auth session.', e?.message || e)
return null
}
})
ipcMain.handle('auth-session-set', async (event, session) => {
try {
if (!keytar) return false
if (!session || typeof session !== 'object') return false
authStore.set(AUTH_SESSION_KEY, serializeAuthSession(session))
await keytar.setPassword(
KEYTAR_SERVICE,
KEYTAR_ACCOUNT,
JSON.stringify(session)
)
return true
} catch (e) {
console.warn('[auth-session] Failed to write auth session.', e?.message || e)
console.warn('[keytar] Failed to write auth session.', e?.message || e)
return false
}
})
ipcMain.handle('auth-session-clear', async () => {
try {
authStore.delete(AUTH_SESSION_KEY)
return true
if (!keytar) return false
return await keytar.deletePassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT)
} catch (e) {
console.warn('[auth-session] Failed to clear auth session.', e?.message || e)
return false
}
})
ipcMain.handle('app-settings-get', async () => {
try {
const settings = appSettingsStore.get(APP_SETTINGS_KEY)
return settings && typeof settings === 'object' ? settings : {}
} catch (e) {
console.warn('[app-settings] Failed to read settings.', e?.message || e)
return {}
}
})
ipcMain.handle('app-settings-set', async (event, settings) => {
try {
if (!settings || typeof settings !== 'object') return false
appSettingsStore.set(APP_SETTINGS_KEY, settings)
return true
} catch (e) {
console.warn('[app-settings] Failed to write settings.', e?.message || e)
console.warn('[keytar] Failed to clear auth session.', e?.message || e)
return false
}
})

View File

@ -1,181 +0,0 @@
import { promises as fs } from 'fs'
import { createRequire } from 'module'
import path from 'path'
const require = createRequire(import.meta.url)
const sudo = require('@vscode/sudo-prompt')
const quoteShellArg = (value) => `'${String(value).replaceAll("'", "'\\''")}'`
const parseMacInstallerProgress = (output) => {
const lines = String(output || '').split('\n')
let percent = null
let message = 'Installing update...'
for (const line of lines) {
if (line.startsWith('installer:PHASE:')) {
message = line.slice('installer:PHASE:'.length).trim() || message
} else if (line.startsWith('installer:STATUS:')) {
const status = line.slice('installer:STATUS:'.length).trim()
if (status) message = status
} else if (line.startsWith('installer:%')) {
const value = Number.parseFloat(line.slice('installer:%'.length))
if (Number.isFinite(value)) {
percent = Math.min(100, Math.round(value <= 1 ? value * 100 : value))
}
} else if (
line.startsWith('installer: ') &&
!line.startsWith('installer:PHASE:') &&
!line.startsWith('installer:STATUS:') &&
!line.startsWith('installer:%')
) {
const text = line.slice('installer: '.length).trim()
if (text) message = text
}
}
return { percent, message }
}
const isMacInstallSuccessful = (output) =>
/installer: The (install|upgrade) was successful\./i.test(output)
const isMacInstallFailed = (output) =>
/installer: The install failed/i.test(output)
const buildMacInstallScript = (installerPath, logPath) =>
`sleep 2 && /usr/sbin/installer -pkg ${quoteShellArg(
installerPath
)} -target / -verboseR 2>&1 | /usr/bin/tee ${quoteShellArg(logPath)}`
const startMacInstallerProgressWatch = (logPath, webContents, sendProgress) => {
let installerOutput = ''
let offset = 0
let lastPercent = null
let lastMessage = null
const poll = async () => {
try {
const stat = await fs.stat(logPath)
if (stat.size <= offset) return
const handle = await fs.open(logPath, 'r')
try {
const buffer = Buffer.alloc(stat.size - offset)
await handle.read(buffer, 0, buffer.length, offset)
offset = stat.size
installerOutput += buffer.toString('utf8')
const { percent, message } = parseMacInstallerProgress(installerOutput)
const resolvedMessage = message || 'Installing update...'
if (percent !== lastPercent || resolvedMessage !== lastMessage) {
lastPercent = percent
lastMessage = resolvedMessage
sendProgress(webContents, {
phase: 'installing',
percent,
message: resolvedMessage
})
}
} finally {
await handle.close()
}
} catch (error) {
if (error?.code !== 'ENOENT') {
console.error('[app-update] installer log poll error:', error)
}
}
}
const intervalId = setInterval(() => {
poll().catch((error) => {
console.error('[app-update] installer log poll error:', error)
})
}, 300)
return async () => {
clearInterval(intervalId)
await poll()
return installerOutput
}
}
export const launchMacInstaller = (
app,
installerPath,
webContents,
{ sendProgress, getInstallErrorMessage }
) => {
const logPath = path.join(path.dirname(installerPath), 'install.log')
const installScript = buildMacInstallScript(installerPath, logPath)
const promptName = 'farmcontrol'
console.log('[app-update] launching macOS installer:', {
installerPath,
installScript,
logPath,
promptName
})
sendProgress(webContents, {
phase: 'installing',
percent: 0,
message: 'Enter your Mac password when prompted.'
})
app.focus({ steal: true })
app.dock?.show()
const stopProgressWatch = startMacInstallerProgressWatch(
logPath,
webContents,
sendProgress
)
return new Promise((resolve, reject) => {
sudo.exec(installScript, { name: promptName }, async (error, stdout, stderr) => {
const watchedOutput = await stopProgressWatch()
const output = `${stdout || ''}${stderr || ''}` || watchedOutput
await fs.unlink(logPath).catch(() => {})
if (stdout) console.log('[app-update] installer stdout:', stdout)
if (stderr) console.error('[app-update] installer stderr:', stderr)
if (error) {
console.error('[app-update] installer error:', error)
const message = getInstallErrorMessage(error, output)
sendProgress(webContents, {
phase: 'error',
percent: null,
message
})
reject(new Error(message))
return
}
if (isMacInstallFailed(output) || !isMacInstallSuccessful(output)) {
const message = getInstallErrorMessage(null, output)
sendProgress(webContents, {
phase: 'error',
percent: null,
message
})
reject(new Error(message))
return
}
const { percent, message } = parseMacInstallerProgress(output)
sendProgress(webContents, {
phase: 'installing',
percent: percent ?? 100,
message: message || 'Installation complete. Restarting Farm Control...'
})
console.log('[app-update] installer completed successfully')
resolve()
})
})
}

View File

@ -6,143 +6,6 @@ const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
let win
let sidebarViewMenuSections = []
const PROTOCOL_PREFIX = 'farmcontrol://'
function findProtocolUrl(args) {
return args.find(
(arg) => typeof arg === 'string' && arg.startsWith(PROTOCOL_PREFIX)
)
}
function sendNavigateToRenderer(redirectPath) {
const deliver = () => {
win.webContents.send('navigate', redirectPath)
win.show()
win.focus()
}
if (!win || win.isDestroyed()) {
createWindow()
win.webContents.once('did-finish-load', () => {
setTimeout(deliver, 100)
})
return
}
if (win.webContents.isLoading()) {
win.webContents.once('did-finish-load', () => {
setTimeout(deliver, 100)
})
return
}
deliver()
}
function toElectronSidebarMenuItems(items = []) {
return items
.map((item) => {
if (item?.type === 'divider') {
return { type: 'separator' }
}
const menuItem = {
label: item.label
}
if (item?.children && Array.isArray(item.children) && item.children.length) {
menuItem.submenu = toElectronSidebarMenuItems(item.children)
} else if (item?.path) {
menuItem.click = () => sendNavigateToRenderer(item.path)
} else {
menuItem.enabled = false
}
return menuItem
})
.filter(Boolean)
}
function buildApplicationMenuTemplate() {
const env = (process.env.NODE_ENV || 'development').trim()
const viewSubmenu = sidebarViewMenuSections.map((section) => ({
label: section.label,
submenu: toElectronSidebarMenuItems(section.items || [])
}))
if (viewSubmenu.length === 0) {
viewSubmenu.push({ label: 'No sidebar items available', enabled: false })
}
if (env === 'development') {
viewSubmenu.push(
{ type: 'separator' },
{
label: 'Toggle Developer Tools',
accelerator:
process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
click: () => {
if (win && !win.isDestroyed()) {
win.webContents.toggleDevTools()
}
}
}
)
}
const template = [
{ role: 'fileMenu' },
{ role: 'editMenu' },
{ label: 'View', submenu: viewSubmenu },
{ role: 'windowMenu' }
]
if (process.platform === 'darwin') {
template.unshift({ role: 'appMenu' })
}
return template
}
function applyApplicationMenu() {
const menu = Menu.buildFromTemplate(buildApplicationMenuTemplate())
Menu.setApplicationMenu(menu)
}
export function handleDeepLink(url) {
if (!url?.startsWith(`${PROTOCOL_PREFIX}app`)) return
const redirectPath = url.replace(`${PROTOCOL_PREFIX}app`, '') || '/'
sendNavigateToRenderer(redirectPath)
}
export function handleDeepLinkFromArgv() {
if (process.platform === 'darwin') return
const url = findProtocolUrl(process.argv)
if (url) handleDeepLink(url)
}
export function setupSingleInstanceLock(app) {
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
return false
}
app.on('second-instance', (_event, commandLine) => {
const url = findProtocolUrl(commandLine)
if (url) {
handleDeepLink(url)
} else if (win && !win.isDestroyed()) {
if (win.isMinimized()) win.restore()
win.show()
win.focus()
}
})
return true
}
function attachKeyboardShortcuts(browserWindow) {
if (!browserWindow) return
@ -189,7 +52,29 @@ export function createWindow() {
}
})
applyApplicationMenu()
// Set up custom menu bar
const env = (process.env.NODE_ENV || 'development').trim()
if (env === 'development') {
const devMenu = [
{
label: 'Developer',
submenu: [
{
label: 'Toggle Developer Tools',
accelerator:
process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
click: () => {
win.webContents.toggleDevTools()
}
}
]
}
]
const menu = Menu.buildFromTemplate(devMenu)
Menu.setApplicationMenu(menu)
} else {
Menu.setApplicationMenu(null)
}
// For development, load from localhost; for production, load the built index.html
if (process.env.ELECTRON_START_URL) {
@ -206,10 +91,6 @@ export function getWindow() {
return win
}
export function getElectronVersion() {
return process.versions.electron
}
export function setupMainWindowIPC() {
// IPC handler to get window state
ipcMain.handle('window-state', () => {
@ -261,18 +142,6 @@ export function setupMainWindowIPC() {
}
return true
})
ipcMain.handle('set-sidebar-view-menu', (event, sidebarSections) => {
if (!Array.isArray(sidebarSections)) {
return false
}
sidebarViewMenuSections = sidebarSections
applyApplicationMenu()
return true
})
ipcMain.handle('electron-version', () => getElectronVersion())
}
export function setupMainWindowAppEvents(app) {
@ -286,7 +155,13 @@ export function setupMainWindowAppEvents(app) {
app.on('open-url', (event, url) => {
event.preventDefault()
handleDeepLink(url)
if (url.startsWith('farmcontrol://app')) {
// Extract the path/query after 'farmcontrol://app'
const redirectPath = url.replace('farmcontrol://app', '') || '/'
if (win && win.webContents) {
win.webContents.send('navigate', redirectPath)
}
}
})
}

View File

@ -1,449 +0,0 @@
import { spawn } from 'child_process'
import { promises as fs } from 'fs'
import os from 'os'
import path from 'path'
import process from 'process'
const MSI_OLE_HEADER = Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1])
const DEBUG_PREFIX = '[app-update][win-progress]'
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const debugLog = (message, details) => {
if (details === undefined) {
console.log(`${DEBUG_PREFIX} ${message}`)
return
}
console.log(`${DEBUG_PREFIX} ${message}`, details)
}
const decodeMsiLogBuffer = (buffer) => {
if (!buffer?.length) return ''
if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) {
debugLog('decoded MSI log as UTF-16 LE (BOM)')
return buffer.subarray(2).toString('utf16le')
}
const sample = buffer.subarray(0, Math.min(buffer.length, 64))
const looksUtf16 =
sample.length >= 4 &&
sample.filter((byte) => byte === 0).length > sample.length / 4
if (looksUtf16) {
debugLog('decoded MSI log as UTF-16 LE (heuristic)')
return buffer.toString('utf16le')
}
debugLog('decoded MSI log as UTF-8')
return buffer.toString('utf8')
}
const formatMsiActionName = (actionName) => {
const humanized = String(actionName)
.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/_/g, ' ')
.toLowerCase()
.trim()
if (!humanized) return 'Installing update...'
return `${humanized.charAt(0).toUpperCase()}${humanized.slice(1)}...`
}
const parseWindowsInstallerProgress = (output) => {
const lines = String(output || '').split(/\r?\n/)
let percent = null
let message = 'Installing update...'
let totalTicks = 0
let currentTicks = 0
let actionStarts = 0
let actionEnds = 0
const matchedLines = []
for (const line of lines) {
const actionStart = line.match(/^Action start \d{2}:\d{2}:\d{2}: (.+?)\./)
if (actionStart) {
actionStarts += 1
message = formatMsiActionName(actionStart[1])
matchedLines.push(`action-start:${actionStart[1]}`)
}
const doingAction = line.match(/Doing action:\s*(.+)$/)
if (doingAction && !actionStart) {
message = formatMsiActionName(doingAction[1])
matchedLines.push(`doing-action:${doingAction[1]}`)
}
if (/^Action ended \d{2}:\d{2}:\d{2}: .+?\. Return value \d+\./.test(line)) {
actionEnds += 1
matchedLines.push('action-ended')
}
const progressReset = line.match(/^\s*0\s+(\d+)\s+0(?:\s+\d+)?\s*$/)
if (progressReset) {
totalTicks = Number.parseInt(progressReset[1], 10) || 0
currentTicks = 0
matchedLines.push(`progress-reset:${totalTicks}`)
}
const progressIncrement = line.match(/^\s*2\s+(\d+)\s*$/)
if (progressIncrement) {
currentTicks += Number.parseInt(progressIncrement[1], 10) || 0
matchedLines.push(`progress-increment:${progressIncrement[1]}`)
}
const progressAddition = line.match(/^\s*3\s+(\d+)\s*$/)
if (progressAddition) {
totalTicks += Number.parseInt(progressAddition[1], 10) || 0
matchedLines.push(`progress-addition:${progressAddition[1]}`)
}
if (/Installation success or error status:\s*0\b/.test(line)) {
percent = 100
message = 'Installation complete. Restarting Farm Control...'
matchedLines.push('install-success')
}
}
if (percent !== 100) {
if (totalTicks > 0) {
percent = Math.min(99, Math.round((currentTicks / totalTicks) * 100))
} else if (actionStarts > 0) {
percent = Math.min(
95,
Math.max(5, Math.round((actionEnds / actionStarts) * 90))
)
}
}
return {
percent,
message,
stats: {
lineCount: lines.length,
actionStarts,
actionEnds,
totalTicks,
currentTicks,
matchedLines: matchedLines.slice(-8)
}
}
}
const isWindowsInstallSuccessful = (output) =>
/Installation success or error status:\s*0\b/.test(output) ||
/MainEngineThread is returning 0\b/.test(output)
const isWindowsInstallFailed = (output) =>
/Installation success or error status:\s*[1-9]\d*\b/.test(output) ||
/MainEngineThread is returning [1-9]\d*\b/.test(output)
const isValidMsiPackage = async (filePath) => {
const handle = await fs.open(filePath, 'r')
try {
const header = Buffer.alloc(MSI_OLE_HEADER.length)
await handle.read(header, 0, header.length, 0)
return header.equals(MSI_OLE_HEADER)
} finally {
await handle.close()
}
}
const prepareInstallerPath = async (installerPath) => {
const fileName = path.basename(installerPath)
const updateDir = path.join(
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
'FarmControl',
'Updates'
)
await fs.mkdir(updateDir, { recursive: true })
const stablePath = path.join(updateDir, fileName)
await fs.copyFile(installerPath, stablePath)
// Resolve to a canonical long path. Short 8.3 paths (e.g. ADMINI~1) break msiexec.
const resolvedPath = await fs.realpath(stablePath)
const stats = await fs.stat(resolvedPath)
if (!stats.isFile() || stats.size === 0) {
throw new Error('Update installer file is missing or empty.')
}
if (!(await isValidMsiPackage(resolvedPath))) {
throw new Error(
'Downloaded update is not a valid Windows Installer package. The file may be corrupted or incomplete.'
)
}
return resolvedPath
}
const startWindowsInstallerProgressWatch = (
logPath,
webContents,
sendProgress
) => {
let installerOutput = ''
let lastLogSize = 0
let lastPercent = null
let lastMessage = null
let pollCount = 0
const poll = async () => {
pollCount += 1
try {
const stat = await fs.stat(logPath)
if (stat.size === 0) {
debugLog(`poll #${pollCount}: log exists but is empty`, { logPath })
return
}
if (stat.size === lastLogSize) {
debugLog(`poll #${pollCount}: no new log data`, {
logPath,
size: stat.size
})
return
}
const buffer = Buffer.alloc(stat.size)
const handle = await fs.open(logPath, 'r')
try {
await handle.read(buffer, 0, stat.size, 0)
} finally {
await handle.close()
}
lastLogSize = stat.size
installerOutput = decodeMsiLogBuffer(buffer)
const { percent, message, stats } =
parseWindowsInstallerProgress(installerOutput)
const resolvedPercent = percent ?? lastPercent ?? 0
const resolvedMessage = message || 'Installing update...'
debugLog(`poll #${pollCount}: parsed installer log`, {
logPath,
size: stat.size,
textLength: installerOutput.length,
preview: installerOutput.slice(0, 240).replace(/\s+/g, ' '),
parsed: stats,
resolvedPercent,
resolvedMessage
})
if (
resolvedPercent !== lastPercent ||
resolvedMessage !== lastMessage
) {
debugLog(`poll #${pollCount}: sending progress update`, {
percent: resolvedPercent,
message: resolvedMessage
})
lastPercent = resolvedPercent
lastMessage = resolvedMessage
sendProgress(webContents, {
phase: 'installing',
percent: resolvedPercent,
message: resolvedMessage
})
} else {
debugLog(`poll #${pollCount}: progress unchanged, skipping UI update`, {
percent: resolvedPercent,
message: resolvedMessage
})
}
} catch (error) {
if (error?.code === 'ENOENT') {
debugLog(`poll #${pollCount}: log file not created yet`, { logPath })
return
}
console.error(`${DEBUG_PREFIX} installer log poll error:`, error)
}
}
const intervalId = setInterval(() => {
poll().catch((error) => {
console.error(`${DEBUG_PREFIX} installer log poll error:`, error)
})
}, 300)
return async () => {
clearInterval(intervalId)
await poll()
debugLog('stopped progress watch', {
logPath,
finalSize: lastLogSize,
textLength: installerOutput.length,
pollCount
})
return installerOutput
}
}
export const launchWindowsInstaller = async (
app,
installerPath,
webContents,
{ sendProgress, getInstallErrorMessage }
) => {
const resolvedPath = await prepareInstallerPath(installerPath)
const logPath = path.join(path.dirname(resolvedPath), 'install.log')
debugLog('prepared installer', {
installerPath,
resolvedPath,
logPath
})
sendProgress(webContents, {
phase: 'installing',
percent: 0,
message: 'Installing update...'
})
await fs.unlink(logPath).catch(() => {})
// Allow file handles from the download/copy to settle before msiexec opens the MSI.
await sleep(2000)
const stopProgressWatch = startWindowsInstallerProgressWatch(
logPath,
webContents,
sendProgress
)
return new Promise((resolve, reject) => {
let processOutput = ''
const startedAt = Date.now()
const installerArgs = [
'/i',
resolvedPath,
'/qn',
'/norestart',
'/L*v!',
logPath
]
debugLog('spawning msiexec', {
args: installerArgs,
elapsedMs: Date.now() - startedAt
})
const installerProcess = spawn('msiexec.exe', installerArgs, {
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true
})
installerProcess.stdout?.on('data', (data) => {
const text = data.toString('utf16le')
processOutput += text
debugLog('msiexec stdout chunk', {
length: text.length,
preview: text.slice(0, 200)
})
})
installerProcess.stderr?.on('data', (data) => {
const text = data.toString('utf16le')
processOutput += text
debugLog('msiexec stderr chunk', {
length: text.length,
preview: text.slice(0, 200)
})
})
installerProcess.on('spawn', () => {
debugLog('msiexec spawned', {
pid: installerProcess.pid,
elapsedMs: Date.now() - startedAt
})
})
installerProcess.on('error', async (error) => {
console.error(`${DEBUG_PREFIX} installer spawn error:`, error)
const watchedOutput = await stopProgressWatch()
debugLog('installer spawn failed', {
watchedOutputLength: watchedOutput.length,
processOutputLength: processOutput.length
})
const message = error?.message || 'Failed to start update installer.'
sendProgress(webContents, {
phase: 'error',
percent: null,
message
})
reject(error)
})
installerProcess.on('exit', async (code, signal) => {
const watchedOutput = await stopProgressWatch()
const output = watchedOutput || processOutput
const finalParse = parseWindowsInstallerProgress(output)
debugLog('msiexec exited', {
code,
signal,
elapsedMs: Date.now() - startedAt,
watchedOutputLength: watchedOutput.length,
processOutputLength: processOutput.length,
parsed: finalParse.stats,
outputPreview: output.slice(0, 500).replace(/\s+/g, ' ')
})
debugLog('keeping install log', { logPath })
if (code !== 0) {
const message = getInstallErrorMessage(null, output)
sendProgress(webContents, {
phase: 'error',
percent: null,
message
})
reject(new Error(message))
return
}
const succeeded =
isWindowsInstallSuccessful(output) ||
(code === 0 && !isWindowsInstallFailed(output))
debugLog('install success evaluation', {
succeeded,
isSuccessful: isWindowsInstallSuccessful(output),
isFailed: isWindowsInstallFailed(output),
exitCode: code
})
if (!succeeded) {
const message = getInstallErrorMessage(null, output)
sendProgress(webContents, {
phase: 'error',
percent: null,
message
})
reject(new Error(message))
return
}
const { percent, message } = finalParse
sendProgress(webContents, {
phase: 'installing',
percent: percent ?? 100,
message: message || 'Installation complete. Restarting Farm Control...'
})
debugLog('installer completed successfully')
resolve()
})
})
}

View File

@ -17,7 +17,6 @@ import {
ElectronSpotlightContentPage
} from './components/Dashboard/context/SpotlightContext.jsx'
import { ActionsModalProvider } from './components/Dashboard/context/ActionsModalContext.jsx'
import MissingPlaceholder from './components/Dashboard/common/MissingPlaceholder.jsx'
import {
ThemeProvider,
@ -26,13 +25,9 @@ import {
import AppError from './components/App/AppError'
import { ApiServerProvider } from './components/Dashboard/context/ApiServerContext.jsx'
import { NotificationProvider } from './components/Dashboard/context/NotificationContext.jsx'
import { ElectronProvider } from './components/Dashboard/context/ElectronContext.jsx'
import { ElectrobunProvider } from './components/Dashboard/context/ElectrobunContext.jsx'
import { MessageProvider } from './components/Dashboard/context/MessageContext.jsx'
import { AppUpdateProvider } from './components/Dashboard/context/AppUpdateContext.jsx'
import AuthCallback from './components/App/AuthCallback.jsx'
import EmailNotificationTemplate from './components/Email/EmailNotificationTemplate.jsx'
import MarketplaceAuthCallback from './components/Dashboard/Sales/Marketplaces/MarketplaceAuthCallback.jsx'
import AuthLaunch from './components/App/AppLaunch.jsx'
import {
ProductionRoutes,
@ -58,34 +53,18 @@ const AppContent = () => {
const Router = getRouter()
return (
<ConfigProvider
theme={themeConfig}
renderEmpty={() => (
<div style={{ margin: '32px' }}>
<MissingPlaceholder
message='No data.'
hasBackground={false}
hasBorder={false}
/>
</div>
)}
>
<ConfigProvider theme={themeConfig}>
<App>
<Router>
<ElectronProvider>
<ElectrobunProvider>
<AuthProvider>
<PrintServerProvider>
<ApiServerProvider>
<MessageProvider>
<AppUpdateProvider>
<NotificationProvider>
<SpotlightProvider>
<ActionsModalProvider>
<Routes>
<Route
path='/applaunch'
element={<AuthLaunch />}
/>
<Route
path='/dashboard/electron/spotlightcontent'
element={
@ -113,21 +92,10 @@ const AppContent = () => {
path='/auth/callback'
element={<AuthCallback />}
/>
<Route
path='/auth/marketplace/callback'
element={<MarketplaceAuthCallback />}
/>
<Route
path='/email/notification'
element={<EmailNotificationTemplate />}
/>
<Route
path='/dashboard'
element={
<PrivateRoute
component={() => <Dashboard />}
/>
<PrivateRoute component={() => <Dashboard />} />
}
>
{ProductionRoutes}
@ -150,12 +118,11 @@ const AppContent = () => {
</ActionsModalProvider>
</SpotlightProvider>
</NotificationProvider>
</AppUpdateProvider>
</MessageProvider>
</ApiServerProvider>
</PrintServerProvider>
</AuthProvider>
</ElectronProvider>
</ElectrobunProvider>
</Router>
</App>
</ConfigProvider>

157
src/bun/index.ts Normal file
View File

@ -0,0 +1,157 @@
import Electrobun, {
BrowserWindow,
BrowserView,
Utils,
GlobalShortcut,
ApplicationMenu
} from 'electrobun/bun'
import { join } from 'path'
import { type FarmControlRPCType } from '../shared/rpcTypes'
import { createMainWindow, getMainWindow } from './mainWindow'
import {
openSpotlightContentWindow,
registerGlobalShortcuts,
setupSpotlightRPC
} from './spotlightWindow'
// Auth session storage (file-based, keytar alternative for Bun)
const AUTH_FILE = join(Utils.paths.userData, 'auth-session.json')
async function readAuthSession(): Promise<object | null> {
try {
const f = Bun.file(AUTH_FILE)
if (!(await f.exists())) return null
const raw = await f.text()
return JSON.parse(raw) as object
} catch {
return null
}
}
async function writeAuthSession(session: object): Promise<boolean> {
try {
await Bun.write(AUTH_FILE, JSON.stringify(session))
return true
} catch (e) {
console.warn('[auth] Failed to write session:', e)
return false
}
}
async function clearAuthSession(): Promise<boolean> {
try {
const f = Bun.file(AUTH_FILE)
if (await f.exists()) {
await Bun.write(AUTH_FILE, '{}')
}
return true
} catch (e) {
console.warn('[auth] Failed to clear session:', e)
return false
}
}
const farmControlRPC = BrowserView.defineRPC<FarmControlRPCType>({
maxRequestTime: 5000,
handlers: {
requests: {
osInfo: () => ({ platform: process.platform }),
windowState: () => {
const win = getMainWindow()
if (!win || win.isDestroyed?.()) {
return { isFullScreen: false, isMaximized: false }
}
return {
isFullScreen: win.isFullScreen?.(),
isMaximized: win.isMaximized?.()
}
},
openExternalUrl: ({ url }) => {
Utils.openExternal(url)
},
openInternalUrl: ({ url }) => {
const win = getMainWindow()
if (!win || win.isDestroyed?.()) {
createMainWindow(farmControlRPC)
const newWin = getMainWindow()
if (newWin?.webview) {
newWin.webview.on?.('dom-ready', () => {
;(newWin.webview as any).rpc?.navigate?.({ url })
newWin.show?.()
newWin.focus?.()
})
}
} else if (win.webview) {
;(win.webview as any).rpc?.navigate?.({ url })
win.show?.()
win.focus?.()
}
return true
},
windowControl: ({ action }) => {
const win = getMainWindow()
if (!win) return
switch (action) {
case 'minimize':
win.minimize?.()
break
case 'maximize':
if (win.isMaximized?.()) {
win.unmaximize?.()
} else {
win.maximize?.()
}
break
case 'close':
win.close?.()
break
}
},
authSessionGet: () => readAuthSession(),
authSessionSet: ({ session }) => writeAuthSession(session),
authSessionClear: () => clearAuthSession(),
spotlightWindowResize: ({ height }) => setupSpotlightRPC(height)
},
messages: {}
}
})
createMainWindow(farmControlRPC)
registerGlobalShortcuts()
// Application menu
const env = (process.env.NODE_ENV || 'development').trim()
if (env === 'development') {
ApplicationMenu.setApplicationMenu([
{
label: 'Developer',
submenu: [
{
label: 'Toggle Developer Tools',
accelerator:
process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
action: 'toggle-devtools'
}
]
}
])
} else {
ApplicationMenu.setApplicationMenu([])
}
// open-url for farmcontrol:// scheme
Electrobun.events.on('open-url', (e: { data: { url: string } }) => {
const url = e.data.url
if (url.startsWith('farmcontrol://app')) {
const redirectPath = url.replace('farmcontrol://app', '') || '/'
const win = getMainWindow()
if (win?.webview) {
;(win.webview as any).rpc?.navigate?.({ url: redirectPath })
}
}
})
// Unregister shortcuts on quit
Electrobun.events.on('before-quit', () => {
GlobalShortcut.unregisterAll()
})

90
src/bun/mainWindow.ts Normal file
View File

@ -0,0 +1,90 @@
import Electrobun, { BrowserWindow, Utils } from 'electrobun/bun'
import { join } from 'path'
import type { FarmControlRPCType } from '../shared/rpcTypes'
let win: InstanceType<typeof BrowserWindow> | null = null
function getAppUrl(): string {
if (process.env.ELECTROBUN_START_URL || process.env.ELECTRON_START_URL) {
return (
process.env.ELECTROBUN_START_URL ||
process.env.ELECTRON_START_URL ||
'http://localhost:5173'
)
}
return 'views://main/index.html'
}
function setupWindowEvents(
browserWindow: InstanceType<typeof BrowserWindow>,
rpc: any
): void {
if (!browserWindow) return
browserWindow.on?.('resize', () => {
const webview = browserWindow.webview
if (webview?.rpc) {
try {
;(webview.rpc as any).windowState?.({
isMaximized: browserWindow.isMaximized?.(),
isFullScreen: browserWindow.isFullScreen?.()
})
} catch (_) {}
}
})
// Electrobun may use different event names - check docs
const sendWindowState = (state: { isMaximized?: boolean; isFullScreen?: boolean }) => {
const webview = browserWindow.webview
if (webview?.rpc) {
try {
;(webview.rpc as any).windowState?.(state)
} catch (_) {}
}
}
// Listen for maximize/unmaximize/fullscreen - Electrobun BrowserWindow events
browserWindow.on?.('maximize', () => sendWindowState({ isMaximized: true }))
browserWindow.on?.('unmaximize', () => sendWindowState({ isMaximized: false }))
browserWindow.on?.('enter-full-screen', () => sendWindowState({ isFullScreen: true }))
browserWindow.on?.('leave-full-screen', () => sendWindowState({ isFullScreen: false }))
}
export function createMainWindow(rpc: any): void {
const url = getAppUrl()
win = new BrowserWindow({
title: 'Farm Control',
url,
frame: {
width: 1200,
height: 800
},
titleBarStyle: 'hiddenInset',
preload: 'views://preload/index.js',
rpc,
styleMask: {
Titled: true,
Closable: true,
Miniaturizable: true,
Resizable: true,
FullSizeContentView: true
}
})
setupWindowEvents(win, rpc)
// Handle window-all-closed
Electrobun.events.on('close', () => {
const wins = (BrowserWindow as any).getAll?.() ?? []
if (wins.length <= 1) {
if (process.platform !== 'darwin') {
Utils.quit?.()
}
}
})
}
export function getMainWindow(): InstanceType<typeof BrowserWindow> | null {
return win
}

View File

@ -0,0 +1,80 @@
import { BrowserWindow, GlobalShortcut } from 'electrobun/bun'
import { join } from 'path'
let spotlightWin: InstanceType<typeof BrowserWindow> | null = null
function getSpotlightRouteUrl(): string {
const routePath = '/dashboard/electron/spotlightcontent'
if (process.env.ELECTROBUN_START_URL || process.env.ELECTRON_START_URL) {
const base = String(
process.env.ELECTROBUN_START_URL || process.env.ELECTRON_START_URL
).replace(/\/$/, '')
return `${base}${routePath}`
}
return `views://main/index.html#${routePath}`
}
export function openSpotlightContentWindow(): void {
if (spotlightWin && !spotlightWin.isDestroyed?.()) {
spotlightWin.show?.()
spotlightWin.focus?.()
return
}
const target = getSpotlightRouteUrl()
spotlightWin = new BrowserWindow({
title: 'Spotlight',
url: target,
frame: {
width: 700,
height: 40
},
titleBarStyle: 'hidden',
transparent: true,
resizable: false
})
spotlightWin.on?.('close', (e: any) => {
if (e?.preventDefault) e.preventDefault()
if (spotlightWin && !spotlightWin.isDestroyed?.()) {
spotlightWin.hide?.()
}
})
spotlightWin.on?.('blur', () => {
if (spotlightWin && !spotlightWin.isDestroyed?.()) {
spotlightWin.hide?.()
}
})
}
export function registerGlobalShortcuts(): void {
try {
const success = GlobalShortcut.register('Alt+Shift+Q', () => {
openSpotlightContentWindow()
})
if (!success) {
console.warn('[GlobalShortcut] Failed to register Alt+Shift+Q')
}
} catch (e) {
console.warn('[GlobalShortcut] Error:', (e as Error)?.message)
}
}
export function setupSpotlightRPC(height: number): boolean {
if (!spotlightWin || spotlightWin.isDestroyed?.()) return false
try {
const frame = spotlightWin.getFrame?.()
if (frame) {
spotlightWin.setSize?.(frame.width, height)
spotlightWin.center?.()
}
return true
} catch (e) {
console.warn('[spotlight] Failed to resize:', (e as Error)?.message)
return false
}
}

View File

@ -1,204 +0,0 @@
import { useContext, useEffect, useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Flex, Card, Alert, Button } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import { customAlphabet } from 'nanoid'
import AuthParticles from './AppParticles'
import FarmControlLogo from '../Logos/FarmControlLogo'
import ExclamationOctagonIcon from '../Icons/ExclamationOctagonIcon'
import CheckIcon from '../Icons/CheckIcon'
import ReloadIcon from '../Icons/ReloadIcon'
import { ApiServerContext } from '../Dashboard/context/ApiServerContext'
const createLaunchSession = customAlphabet(
'01abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
32
)
const AuthLaunch = () => {
const location = useLocation()
const hasRedirected = useRef(false)
const startTimeoutRef = useRef(null)
const pollTimeoutRef = useRef(null)
const { getAppLaunchSession } = useContext(ApiServerContext)
const [launchError, setLaunchError] = useState(false)
const [launchErrorMessage, setLaunchErrorMessage] = useState('')
const [launchSuccess, setLaunchSuccess] = useState(false)
const handleRefresh = () => {
window.location.reload()
}
useEffect(() => {
let cancelled = false
const redirect = new URLSearchParams(location.search).get('redirect')
const redirectType = new URLSearchParams(location.search).get(
'redirectType'
)
if (!redirect) {
setLaunchError(true)
setLaunchErrorMessage('No redirect provided!')
return
}
if (!redirect || hasRedirected.current) {
return
}
startTimeoutRef.current = setTimeout(() => {
if (cancelled) {
return
}
hasRedirected.current = true
const launchSession = createLaunchSession()
let launchCheckCount = 0
setLaunchError(false)
setLaunchErrorMessage('')
setLaunchSuccess(false)
let redirectWithLaunchSession = redirect
try {
const redirectUrl = new URL(redirect, window.location.origin)
redirectUrl.searchParams.set('launchSession', launchSession)
redirectWithLaunchSession = redirectUrl.toString()
} catch {
const hasQuery = redirect.includes('?')
const separator = hasQuery ? '&' : '?'
redirectWithLaunchSession = `${redirect}${separator}launchSession=${encodeURIComponent(
launchSession
)}`
}
const link = document.createElement('a')
link.href = redirectWithLaunchSession
link.style.display = 'none'
if (redirectType === 'app-localhost') {
link.addEventListener('click', (event) => {
event.preventDefault()
window.open(
redirectWithLaunchSession,
'farmcontrol-launch',
'width=480,height=640,menubar=no,toolbar=no,location=yes,status=no,resizable=yes,scrollbars=yes'
)
})
}
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
const checkLaunchSession = async () => {
launchCheckCount += 1
let launchComplete = false
try {
const launchStatus = await getAppLaunchSession(launchSession)
launchComplete = launchStatus?.complete === true
} catch {
launchComplete = false
}
if (cancelled) {
return
}
if (launchComplete) {
setLaunchSuccess(true)
return
}
if (launchCheckCount >= 10) {
setLaunchError(true)
setLaunchErrorMessage('Failed to open Farm Control.')
return
}
pollTimeoutRef.current = setTimeout(() => {
checkLaunchSession()
}, 1000)
}
checkLaunchSession()
}, 0)
return () => {
cancelled = true
hasRedirected.current = false
if (startTimeoutRef.current) {
clearTimeout(startTimeoutRef.current)
}
if (pollTimeoutRef.current) {
clearTimeout(pollTimeoutRef.current)
}
}
}, [getAppLaunchSession, location.search])
return (
<div
style={{
backgroundColor: 'black'
}}
>
<div
style={{
backgroundColor: 'black',
minHeight: '100vh',
transition: 'opacity 0.5s ease-in-out',
opacity: 1
}}
>
<AuthParticles />
<Flex
align='center'
justify='center'
vertical
style={{ height: '100vh' }}
gap={'large'}
>
<Card style={{ borderRadius: 20 }}>
<Flex vertical align='center'>
<FarmControlLogo style={{ fontSize: '500px', height: '40px' }} />
</Flex>
</Card>
{!launchError && !launchSuccess && (
<Alert
message='Launching Farm Control please wait...'
icon={<LoadingOutlined />}
showIcon
/>
)}
{launchError && (
<>
<Alert
message={launchErrorMessage}
icon={<ExclamationOctagonIcon />}
type='error'
showIcon
/>
<Button
icon={<ReloadIcon />}
onClick={handleRefresh}
size='large'
/>
</>
)}
{launchSuccess && (
<Alert
message='Launch successful! You may now close this window.'
icon={<CheckIcon />}
type='success'
showIcon
/>
)}
</Flex>
</div>
</div>
)
}
export default AuthLaunch

View File

@ -1,14 +1,50 @@
import { useLocation } from 'react-router-dom'
import DashboardSidebar from '../common/DashboardSidebar'
import { getSidebarItems, getSidebarSelectedKey } from '../../../database/Sidebars'
import { Typography } from 'antd'
const { Text } = Typography
const items = [
{
key: 'sessionstorage',
label: 'Session Storage',
icon: <Text>🗃</Text>,
path: '/dashboard/developer/sessionstorage'
},
{
key: 'authcontextdebug',
label: 'Auth Debug',
icon: <Text>🔐</Text>,
path: '/dashboard/developer/authcontextdebug'
},
{
key: 'apicontextdebug',
label: 'API Debug',
icon: <Text>🌐</Text>,
path: '/dashboard/developer/apicontextdebug'
}
]
const routeKeyMap = {
'/dashboard/developer/sessionstorage': 'sessionstorage',
'/dashboard/developer/authcontextdebug': 'authcontextdebug',
'/dashboard/developer/apicontextdebug': 'apicontextdebug'
}
const DeveloperSidebar = (props) => {
const location = useLocation()
const includeDev = import.meta.env.MODE === 'development'
const items = getSidebarItems('developer', { includeDev })
const selectedKey = getSidebarSelectedKey('developer', location.pathname, {
includeDev
const selectedKey = (() => {
const match = Object.keys(routeKeyMap).find((path) => {
const pathSplit = path.split('/')
const locationPathSplit = location.pathname.split('/')
if (pathSplit.length > locationPathSplit.length) return false
for (let i = 0; i < pathSplit.length; i++) {
if (pathSplit[i] !== locationPathSplit[i]) return false
}
return true
})
return match ? routeKeyMap[match] : 'sessionstorage'
})()
return <DashboardSidebar items={items} selectedKey={selectedKey} {...props} />
}

View File

@ -1,10 +1,15 @@
import { useContext } from 'react'
import { Flex } from 'antd'
import useCollapseState from '../hooks/useCollapseState'
import StatsDisplay from '../common/StatsDisplay'
import InfoCollapse from '../common/InfoCollapse'
import ScrollBox from '../common/ScrollBox'
import { ApiServerContext } from '../context/ApiServerContext'
const FinanceOverview = () => {
const { connected } = useContext(ApiServerContext)
const [collapseState, updateCollapseState] = useCollapseState(
'FinanceOverview',
{
@ -13,6 +18,10 @@ const FinanceOverview = () => {
}
)
if (!connected) {
return null
}
return (
<Flex
gap='large'

View File

@ -1,14 +1,51 @@
import { useLocation } from 'react-router-dom'
import DashboardSidebar from '../common/DashboardSidebar'
import { getSidebarItems, getSidebarSelectedKey } from '../../../database/Sidebars'
import InvoiceIcon from '../../Icons/InvoiceIcon'
import PaymentIcon from '../../Icons/PaymentIcon'
import FinanceIcon from '../../Icons/FinanceIcon'
const items = [
{
key: 'overview',
label: 'Overview',
icon: <FinanceIcon />,
path: '/dashboard/finance/overview'
},
{ type: 'divider' },
{
key: 'invoices',
label: 'Invoices',
icon: <InvoiceIcon />,
path: '/dashboard/finance/invoices'
},
{
key: 'payments',
label: 'Payments',
icon: <PaymentIcon />,
path: '/dashboard/finance/payments'
}
]
const routeKeyMap = {
'/dashboard/finance/overview': 'overview',
'/dashboard/finance/invoices': 'invoices',
'/dashboard/finance/payments': 'payments'
}
const FinanceSidebar = (props) => {
const location = useLocation()
const includeDev = import.meta.env.MODE === 'development'
const items = getSidebarItems('finance', { includeDev })
const selectedKey = getSidebarSelectedKey('finance', location.pathname, {
includeDev
const selectedKey = (() => {
const match = Object.keys(routeKeyMap).find((path) => {
const pathSplit = path.split('/')
const locationPathSplit = location.pathname.split('/')
if (pathSplit.length > locationPathSplit.length) return false
for (let i = 0; i < pathSplit.length; i++) {
if (pathSplit[i] !== locationPathSplit[i]) return false
}
return true
})
return match ? routeKeyMap[match] : 'overview'
})()
return <DashboardSidebar items={items} selectedKey={selectedKey} {...props} />
}

View File

@ -5,12 +5,10 @@ import ObjectTable from '../common/ObjectTable'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTableViewButton from '../common/ObjectTableViewButton'
import FilterSidebarButton from '../common/FilterSidebarButton'
import GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode'
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
import ColumnViewButton from '../common/ColumnViewButton'
import ExportListButton from '../common/ExportListButton'
const Invoices = () => {
const [newInvoiceOpen, setNewInvoiceOpen] = useState(false)
@ -21,9 +19,6 @@ const Invoices = () => {
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('invoices')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('Invoices')
const actionItems = {
items: [
{
@ -49,7 +44,7 @@ const Invoices = () => {
return (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex vertical={'true'} gap='large'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
@ -61,16 +56,13 @@ const Invoices = () => {
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='invoice' />
</Space>
<Space>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
@ -79,8 +71,6 @@ const Invoices = () => {
visibleColumns={columnVisibility}
type='invoice'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
expandHeight={true}
/>
</Flex>
<Modal

View File

@ -64,6 +64,10 @@ const InvoiceInfo = () => {
const [newPaymentOpen, setNewPaymentOpen] = useState(false)
const actions = {
reload: () => {
objectFormRef?.current?.handleFetchObject?.()
return true
},
edit: () => {
objectFormRef?.current?.startEditing?.()
return false
@ -309,7 +313,7 @@ const InvoiceInfo = () => {
<PostInvoice
onOk={() => {
setPostInvoiceOpen(false)
objectFormRef?.current.handleFetchObject()
actions.reload()
}}
objectData={objectFormState.objectData}
/>
@ -327,7 +331,7 @@ const InvoiceInfo = () => {
<AcknowledgeInvoice
onOk={() => {
setAcknowledgeInvoiceOpen(false)
objectFormRef?.current.handleFetchObject()
actions.reload()
}}
objectData={objectFormState.objectData}
/>
@ -345,7 +349,7 @@ const InvoiceInfo = () => {
<NewPayment
onOk={() => {
setNewPaymentOpen(false)
objectFormRef?.current.handleFetchObject()
actions.reload()
}}
reset={newPaymentOpen}
defaultValues={{

View File

@ -34,8 +34,6 @@ const NewInvoice = ({ onOk, reset, defaultValues }) => {
order: true,
to: true,
from: true,
toType: true,
fromType: true,
issuedAt: true,
dueAt: true
}}
@ -66,9 +64,7 @@ const NewInvoice = ({ onOk, reset, defaultValues }) => {
sentAt: false,
paidAt: false,
cancelledAt: false,
overdueAt: false,
acknowledgedAt: false,
postedAt: false
overdueAt: false
}}
isEditing={false}
objectData={objectData}

View File

@ -5,12 +5,10 @@ import ObjectTable from '../common/ObjectTable'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTableViewButton from '../common/ObjectTableViewButton'
import FilterSidebarButton from '../common/FilterSidebarButton'
import GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode'
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
import ColumnViewButton from '../common/ColumnViewButton'
import ExportListButton from '../common/ExportListButton'
const Payments = () => {
const [newPaymentOpen, setNewPaymentOpen] = useState(false)
@ -21,9 +19,6 @@ const Payments = () => {
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('payments')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('Payments')
const actionItems = {
items: [
{
@ -49,7 +44,7 @@ const Payments = () => {
return (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex vertical={'true'} gap='large'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
@ -61,16 +56,13 @@ const Payments = () => {
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='payment' />
</Space>
<Space>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
@ -79,8 +71,6 @@ const Payments = () => {
visibleColumns={columnVisibility}
type='payment'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
expandHeight={true}
/>
</Flex>
<Modal
@ -106,3 +96,4 @@ const Payments = () => {
}
export default Payments

View File

@ -1,46 +0,0 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const AuthorisePayment = ({ onOk, objectData }) => {
const [authoriseLoading, setAuthoriseLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handleAuthorise = async () => {
setAuthoriseLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'Payment',
'authorise'
)
if (result) {
message.success('Payment authorised successfully')
onOk(result)
}
} catch (error) {
console.error('Error authorising payment:', error)
} finally {
setAuthoriseLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to authorise this payment?'}
description={`Authorising payment ${objectData?.name || objectData?._reference || objectData?._id} will update its status to authorised.`}
onOk={handleAuthorise}
okText='Authorise'
okLoading={authoriseLoading}
/>
)
}
AuthorisePayment.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default AuthorisePayment

View File

@ -1,46 +0,0 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const CancelPayment = ({ onOk, objectData }) => {
const [cancelLoading, setCancelLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handleCancel = async () => {
setCancelLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'Payment',
'cancel'
)
if (result) {
message.success('Payment cancelled successfully')
onOk(result)
}
} catch (error) {
console.error('Error cancelling payment:', error)
} finally {
setCancelLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to cancel this payment?'}
description={`Cancelling payment ${objectData?.name || objectData?._reference || objectData?._id} will update its status to cancelled.`}
onOk={handleCancel}
okText='Cancel'
okLoading={cancelLoading}
/>
)
}
CancelPayment.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default CancelPayment

View File

@ -1,46 +0,0 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const DeclinePayment = ({ onOk, objectData }) => {
const [declineLoading, setDeclineLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handleDecline = async () => {
setDeclineLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'Payment',
'decline'
)
if (result) {
message.success('Payment declined successfully')
onOk(result)
}
} catch (error) {
console.error('Error declining payment:', error)
} finally {
setDeclineLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to decline this payment?'}
description={`Declining payment ${objectData?.name || objectData?._reference || objectData?._id} will update its status to declined.`}
onOk={handleDecline}
okText='Decline'
okLoading={declineLoading}
/>
)
}
DeclinePayment.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default DeclinePayment

View File

@ -53,8 +53,6 @@ const NewPayment = ({ onOk, reset, defaultValues }) => {
updatedAt: false,
_reference: false,
postedAt: false,
authorisedAt: false,
declinedAt: false,
cancelledAt: false
}}
isEditing={false}

View File

@ -24,9 +24,6 @@ import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
import { getModelByName } from '../../../../database/ObjectModels.js'
import PostPayment from './PostPayment.jsx'
import AuthorisePayment from './AuthorisePayment.jsx'
import DeclinePayment from './DeclinePayment.jsx'
import CancelPayment from './CancelPayment.jsx'
const log = loglevel.getLogger('PaymentInfo')
log.setLevel(config.logLevel)
@ -51,11 +48,12 @@ const PaymentInfo = () => {
objectData: {}
})
const [postPaymentOpen, setPostPaymentOpen] = useState(false)
const [authorisePaymentOpen, setAuthorisePaymentOpen] = useState(false)
const [declinePaymentOpen, setDeclinePaymentOpen] = useState(false)
const [cancelPaymentOpen, setCancelPaymentOpen] = useState(false)
const actions = {
reload: () => {
objectFormRef?.current?.handleFetchObject?.()
return true
},
edit: () => {
objectFormRef?.current?.startEditing?.()
return false
@ -75,18 +73,6 @@ const PaymentInfo = () => {
post: () => {
setPostPaymentOpen(true)
return true
},
authorise: () => {
setAuthorisePaymentOpen(true)
return true
},
decline: () => {
setDeclinePaymentOpen(true)
return true
},
cancel: () => {
setCancelPaymentOpen(true)
return true
}
}
@ -247,61 +233,7 @@ const PaymentInfo = () => {
<PostPayment
onOk={() => {
setPostPaymentOpen(false)
objectFormRef?.current.handleFetchObject()
}}
objectData={objectFormState.objectData}
/>
</Modal>
<Modal
open={authorisePaymentOpen}
onCancel={() => {
setAuthorisePaymentOpen(false)
}}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<AuthorisePayment
onOk={() => {
setAuthorisePaymentOpen(false)
objectFormRef?.current.handleFetchObject()
}}
objectData={objectFormState.objectData}
/>
</Modal>
<Modal
open={declinePaymentOpen}
onCancel={() => {
setDeclinePaymentOpen(false)
}}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<DeclinePayment
onOk={() => {
setDeclinePaymentOpen(false)
objectFormRef?.current.handleFetchObject()
}}
objectData={objectFormState.objectData}
/>
</Modal>
<Modal
open={cancelPaymentOpen}
onCancel={() => {
setCancelPaymentOpen(false)
}}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<CancelPayment
onOk={() => {
setCancelPaymentOpen(false)
objectFormRef?.current.handleFetchObject()
actions.reload()
}}
objectData={objectFormState.objectData}
/>

View File

@ -9,12 +9,10 @@ import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTable from '../common/ObjectTable'
import ObjectTableViewButton from '../common/ObjectTableViewButton'
import FilterSidebarButton from '../common/FilterSidebarButton'
import ListIcon from '../../Icons/ListIcon'
import GridIcon from '../../Icons/GridIcon'
import useViewMode from '../hooks/useViewMode'
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
import ColumnViewButton from '../common/ColumnViewButton'
import ExportListButton from '../common/ExportListButton'
const FilamentStocks = () => {
const tableRef = useRef()
@ -26,9 +24,6 @@ const FilamentStocks = () => {
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('filamentStocks')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('FilamentStocks')
const actionItems = {
items: [
{
@ -54,7 +49,7 @@ const FilamentStocks = () => {
return (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex vertical={'true'} gap='large'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
@ -66,16 +61,13 @@ const FilamentStocks = () => {
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='filamentStock' />
</Space>
<Space>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
@ -85,8 +77,6 @@ const FilamentStocks = () => {
visibleColumns={columnVisibility}
type='filamentStock'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
expandHeight={true}
/>
</Flex>
<Modal

View File

@ -47,6 +47,10 @@ const FilamentStockInfo = () => {
})
const actions = {
reload: () => {
objectFormRef?.current?.fetchObject?.()
return true
},
edit: () => {
objectFormRef?.current?.startEditing?.()
return false

View File

@ -36,7 +36,6 @@ const NewFilamentStock = ({ onOk, reset, defaultValues }) => {
bordered={false}
visibleProperties={{
_id: false,
_reference: false,
createdAt: false,
updatedAt: false
}}

View File

@ -1,10 +1,15 @@
import { useContext } from 'react'
import { Flex } from 'antd'
import useCollapseState from '../hooks/useCollapseState'
import StatsDisplay from '../common/StatsDisplay'
import InfoCollapse from '../common/InfoCollapse'
import ScrollBox from '../common/ScrollBox'
import { ApiServerContext } from '../context/ApiServerContext'
const InventoryOverview = () => {
const { connected } = useContext(ApiServerContext)
const [collapseState, updateCollapseState] = useCollapseState(
'InventoryOverview',
{
@ -13,6 +18,10 @@ const InventoryOverview = () => {
}
)
if (!connected) {
return null
}
return (
<Flex
gap='large'

View File

@ -1,14 +1,102 @@
import { useLocation } from 'react-router-dom'
import DashboardSidebar from '../common/DashboardSidebar'
import { getSidebarItems, getSidebarSelectedKey } from '../../../database/Sidebars'
import FilamentStockIcon from '../../Icons/FilamentStockIcon'
import PartStockIcon from '../../Icons/PartStockIcon'
import ProductStockIcon from '../../Icons/ProductStockIcon'
import StockEventIcon from '../../Icons/StockEventIcon'
import StockAuditIcon from '../../Icons/StockAuditIcon'
import PurchaseOrderIcon from '../../Icons/PurchaseOrderIcon'
import ShipmentIcon from '../../Icons/ShipmentIcon'
import OrderItemIcon from '../../Icons/OrderItemIcon'
import InventoryIcon from '../../Icons/InventoryIcon'
const items = [
{
key: 'overview',
label: 'Overview',
icon: <InventoryIcon />,
path: '/dashboard/inventory/overview'
},
{ type: 'divider' },
{
key: 'filamentstocks',
label: 'Filament Stocks',
icon: <FilamentStockIcon />,
path: '/dashboard/inventory/filamentstocks'
},
{
key: 'partstocks',
label: 'Part Stocks',
icon: <PartStockIcon />,
path: '/dashboard/inventory/partstocks'
},
{
key: 'productstocks',
label: 'Product Stocks',
icon: <ProductStockIcon />,
path: '/dashboard/inventory/productstocks'
},
{ type: 'divider' },
{
key: 'purchaseorders',
label: 'Purchase Orders',
icon: <PurchaseOrderIcon />,
path: '/dashboard/inventory/purchaseorders'
},
{ type: 'divider' },
{
key: 'orderitems',
label: 'Order Items',
icon: <OrderItemIcon />,
path: '/dashboard/inventory/orderitems'
},
{
key: 'shipments',
label: 'Shipments',
icon: <ShipmentIcon />,
path: '/dashboard/inventory/shipments'
},
{ type: 'divider' },
{
key: 'stockevents',
label: 'Stock Events',
icon: <StockEventIcon />,
path: '/dashboard/inventory/stockevents'
},
{
key: 'stockaudits',
label: 'Stock Audits',
icon: <StockAuditIcon />,
path: '/dashboard/inventory/stockaudits'
}
]
const routeKeyMap = {
'/dashboard/inventory/overview': 'overview',
'/dashboard/inventory/filamentstocks': 'filamentstocks',
'/dashboard/inventory/partstocks': 'partstocks',
'/dashboard/inventory/productstocks': 'productstocks',
'/dashboard/inventory/stockevents': 'stockevents',
'/dashboard/inventory/stockaudits': 'stockaudits',
'/dashboard/inventory/purchaseorders': 'purchaseorders',
'/dashboard/inventory/orderitems': 'orderitems',
'/dashboard/inventory/shipments': 'shipments'
}
const InventorySidebar = (props) => {
const location = useLocation()
const includeDev = import.meta.env.MODE === 'development'
const items = getSidebarItems('inventory', { includeDev })
const selectedKey = getSidebarSelectedKey('inventory', location.pathname, {
includeDev
const selectedKey = (() => {
const match = Object.keys(routeKeyMap).find((path) => {
const pathSplit = path.split('/')
const locationPathSplit = location.pathname.split('/')
if (pathSplit.length > locationPathSplit.length) return false
for (let i = 0; i < pathSplit.length; i++) {
if (pathSplit[i] !== locationPathSplit[i]) return false
}
return true
})
return match ? routeKeyMap[match] : 'overview'
})()
return <DashboardSidebar items={items} selectedKey={selectedKey} {...props} />
}

View File

@ -5,12 +5,10 @@ import ObjectTable from '../common/ObjectTable'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTableViewButton from '../common/ObjectTableViewButton'
import FilterSidebarButton from '../common/FilterSidebarButton'
import GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode'
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
import ColumnViewButton from '../common/ColumnViewButton'
import ExportListButton from '../common/ExportListButton'
const OrderItems = () => {
const [newOrderItemOpen, setNewOrderItemOpen] = useState(false)
@ -21,9 +19,6 @@ const OrderItems = () => {
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('orderItems')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('OrderItems')
const actionItems = {
items: [
{
@ -49,7 +44,7 @@ const OrderItems = () => {
return (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex vertical={'true'} gap='large'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
@ -61,16 +56,13 @@ const OrderItems = () => {
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='orderItem' />
</Space>
<Space>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
@ -79,8 +71,6 @@ const OrderItems = () => {
visibleColumns={columnVisibility}
type='orderItem'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
expandHeight={true}
/>
</Flex>
<Modal

View File

@ -46,34 +46,6 @@ const NewOrderItem = ({ onOk, reset, defaultValues }) => {
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='orderItem'
column={1}
bordered={false}
isEditing={true}
required={false}
objectData={objectData}
visibleProperties={{
sku: true,
shipment: true,
invoicedAmount: false,
invoicedAmountWithTax: false,
invoicedQuantity: false,
invoicedAmountRemaining: false,
invoicedAmountWithTaxRemaining: false,
invoicedQuantityRemaining: false,
orderedAt: false,
receivedAt: false,
syncAmount: false
}}
/>
)
},
{
title: 'Pricing',
key: 'pricing',
@ -95,6 +67,32 @@ const NewOrderItem = ({ onOk, reset, defaultValues }) => {
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='orderItem'
column={1}
bordered={false}
isEditing={true}
required={false}
objectData={objectData}
visibleProperties={{
shipment: true,
invoicedAmount: false,
invoicedAmountWithTax: false,
invoicedQuantity: false,
invoicedAmountRemaining: false,
invoicedAmountWithTaxRemaining: false,
invoicedQuantityRemaining: false,
orderedAt: false,
receivedAt: false,
syncAmount: false
}}
/>
)
},
{
title: 'Summary',
key: 'summary',

View File

@ -49,6 +49,10 @@ const OrderItemInfo = () => {
})
const actions = {
reload: () => {
objectFormRef?.current?.handleFetchObject?.()
return true
},
edit: () => {
objectFormRef?.current?.startEditing?.()
return false

View File

@ -9,12 +9,10 @@ import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTable from '../common/ObjectTable'
import ObjectTableViewButton from '../common/ObjectTableViewButton'
import FilterSidebarButton from '../common/FilterSidebarButton'
import ListIcon from '../../Icons/ListIcon'
import GridIcon from '../../Icons/GridIcon'
import useViewMode from '../hooks/useViewMode'
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
import ColumnViewButton from '../common/ColumnViewButton'
import ExportListButton from '../common/ExportListButton'
const PartStocks = () => {
const tableRef = useRef()
@ -26,9 +24,6 @@ const PartStocks = () => {
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('partStocks')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('PartStocks')
const actionItems = {
items: [
{
@ -54,7 +49,7 @@ const PartStocks = () => {
return (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex vertical={'true'} gap='large'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
@ -66,16 +61,13 @@ const PartStocks = () => {
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='partStock' />
</Space>
<Space>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
@ -85,8 +77,6 @@ const PartStocks = () => {
visibleColumns={columnVisibility}
type='partStock'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
expandHeight={true}
/>
</Flex>
<Modal

View File

@ -36,7 +36,6 @@ const NewPartStock = ({ onOk, reset, defaultValues }) => {
bordered={false}
visibleProperties={{
_id: false,
_reference: false,
createdAt: false,
updatedAt: false
}}

View File

@ -51,6 +51,10 @@ const PartStockInfo = () => {
})
const actions = {
reload: () => {
objectFormRef?.current.handleFetchObject()
return true
},
edit: () => {
objectFormRef?.current.startEditing()
return false

View File

@ -1,115 +0,0 @@
// src/components/Dashboard/Inventory/ProductStocks.jsx
// ProductStocks - tracks assembled products consisting of part stocks
import { useState, useRef } from 'react'
import { Button, Flex, Space, Modal, Dropdown } from 'antd'
import NewProductStock from './ProductStocks/NewProductStock'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTable from '../common/ObjectTable'
import ObjectTableViewButton from '../common/ObjectTableViewButton'
import FilterSidebarButton from '../common/FilterSidebarButton'
import useViewMode from '../hooks/useViewMode'
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
import ColumnViewButton from '../common/ColumnViewButton'
import ExportListButton from '../common/ExportListButton'
const ProductStocks = () => {
const tableRef = useRef()
const [newProductStockOpen, setNewProductStockOpen] = useState(false)
const [viewMode, setViewMode] = useViewMode('productStocks')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('productStock')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('ProductStocks')
const actionItems = {
items: [
{
label: 'New Product Stock',
key: 'newProductStock',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newProductStock') {
setNewProductStockOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='productStock'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='productStock' />
</Space>
<Space>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='productStock'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
expandHeight={true}
/>
</Flex>
<Modal
open={newProductStockOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={800}
onCancel={() => {
setNewProductStockOpen(false)
}}
destroyOnHidden={true}
>
<NewProductStock
onOk={() => {
setNewProductStockOpen(false)
tableRef.current?.reload()
}}
reset={newProductStockOpen}
/>
</Modal>
</>
)
}
export default ProductStocks

View File

@ -1,78 +0,0 @@
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewProductStock = ({ onOk, reset, defaultValues }) => {
return (
<NewObjectForm
type={'productStock'}
reset={reset}
defaultValues={{ state: { type: 'draft' }, ...defaultValues }}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='productStock'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
visibleProperties={{
partStocks: false
}}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='productStock'
column={1}
bordered={false}
visibleProperties={{
_id: false,
_reference: false,
createdAt: false,
updatedAt: false,
partStocks: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Product Stock'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewProductStock.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool,
defaultValues: PropTypes.object
}
export default NewProductStock

View File

@ -1,46 +0,0 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const PostProductStock = ({ onOk, objectData }) => {
const [postLoading, setPostLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handlePost = async () => {
setPostLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'ProductStock',
'post'
)
if (result) {
message.success('Product stock posted successfully')
onOk(result)
}
} catch (error) {
console.error('Error posting product stock:', error)
} finally {
setPostLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to post this product stock?'}
description={`Posting product stock ${objectData?.name || objectData?._reference || objectData?._id} will finalize it and make it read-only.`}
onOk={handlePost}
okText='Post'
okLoading={postLoading}
/>
)
}
PostProductStock.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default PostProductStock

View File

@ -1,270 +0,0 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card, Modal } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import loglevel from 'loglevel'
import config from '../../../../config.js'
import useCollapseState from '../../hooks/useCollapseState.jsx'
import NotesPanel from '../../common/NotesPanel.jsx'
import InfoCollapse from '../../common/InfoCollapse.jsx'
import ObjectInfo from '../../common/ObjectInfo.jsx'
import ObjectProperty from '../../common/ObjectProperty.jsx'
import ViewButton from '../../common/ViewButton.jsx'
import { getModelProperty, getModelByName } from '../../../../database/ObjectModels.js'
import PostProductStock from './PostProductStock.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import PartStockIcon from '../../../Icons/PartStockIcon.jsx'
import ObjectForm from '../../common/ObjectForm.jsx'
import EditButtons from '../../common/EditButtons.jsx'
import LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('ProductStockInfo')
log.setLevel(config.logLevel)
const ProductStockInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const productStockId = new URLSearchParams(location.search).get(
'productStockId'
)
const [postProductStockOpen, setPostProductStockOpen] = useState(false)
const [collapseState, updateCollapseState] = useCollapseState(
'ProductStockInfo',
{
info: true,
partStocks: true,
notes: true,
auditLogs: true
}
)
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
locked: false,
loading: false,
objectData: {}
})
const actions = {
edit: () => {
objectFormRef?.current.startEditing()
return false
},
cancelEdit: () => {
objectFormRef?.current.cancelEditing()
return true
},
finishEdit: () => {
objectFormRef?.current.handleUpdate()
return true
},
delete: () => {
objectFormRef?.current.handleDelete?.()
return true
},
post: () => {
setPostProductStockOpen(true)
return true
}
}
const editDisabled =
getModelByName('productStock')
?.actions?.find((action) => action.name === 'edit')
?.disabled(objectFormState.objectData) ?? false
return (
<>
<Flex
gap='large'
vertical='true'
style={{
maxHeight: '100%',
minHeight: 0
}}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='productStock'
id={productStockId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Product Stock Information' },
{ key: 'partStocks', label: 'Part Stocks' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<UserNotifierToggle
type='productStock'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
<DocumentPrintButton
type='productStock'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
actionHandlerRef.current.callAction('finishEdit')
}}
cancelEditing={() => {
actionHandlerRef.current.callAction('cancelEdit')
}}
startEditing={() => {
actionHandlerRef.current.callAction('edit')
}}
editLoading={objectFormState.editLoading}
formValid={objectFormState.formValid}
disabled={
objectFormState.lock?.locked ||
objectFormState.loading ||
editDisabled
}
loading={objectFormState.editLoading}
/>
</Space>
</Flex>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<ObjectForm
id={productStockId}
type='productStock'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<Flex vertical gap={'large'}>
<InfoCollapse
title='Product Stock Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='productStock'
objectData={objectData}
labelWidth='175px'
visibleProperties={{ partStocks: false }}
/>
</InfoCollapse>
<InfoCollapse
title='Part Stocks'
icon={<PartStockIcon />}
active={collapseState.partStocks}
onToggle={(expanded) =>
updateCollapseState('partStocks', expanded)
}
collapseKey='partStocks'
>
<ObjectProperty
{...getModelProperty('productStock', 'partStocks')}
isEditing={isEditing}
objectData={objectData}
loading={loading}
size='medium'
/>
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={productStockId} type='productStock' />
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{ 'parent._id': productStockId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
<Modal
open={postProductStockOpen}
onCancel={() => {
setPostProductStockOpen(false)
}}
width={500}
footer={null}
destroyOnHidden={true}
centered={true}
>
<PostProductStock
onOk={() => {
setPostProductStockOpen(false)
objectFormRef?.current.handleFetchObject()
}}
objectData={objectFormState.objectData}
/>
</Modal>
</>
)
}
export default ProductStockInfo

View File

@ -5,12 +5,10 @@ import ObjectTable from '../common/ObjectTable'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTableViewButton from '../common/ObjectTableViewButton'
import FilterSidebarButton from '../common/FilterSidebarButton'
import GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode'
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
import ColumnViewButton from '../common/ColumnViewButton'
import ExportListButton from '../common/ExportListButton'
const PurchaseOrders = () => {
const [newPurchaseOrderOpen, setNewPurchaseOrderOpen] = useState(false)
@ -21,9 +19,6 @@ const PurchaseOrders = () => {
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('purchaseOrders')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('PurchaseOrders')
const actionItems = {
items: [
{
@ -49,7 +44,7 @@ const PurchaseOrders = () => {
return (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex vertical={'true'} gap='large'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
@ -61,16 +56,13 @@ const PurchaseOrders = () => {
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='purchaseOrder' />
</Space>
<Space>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
@ -79,8 +71,6 @@ const PurchaseOrders = () => {
visibleColumns={columnVisibility}
type='purchaseOrder'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
expandHeight={true}
/>
</Flex>
<Modal

View File

@ -74,6 +74,10 @@ const PurchaseOrderInfo = () => {
})
const actions = {
reload: () => {
objectFormRef?.current?.handleFetchObject?.()
return true
},
edit: () => {
orderItemsTableRef?.current?.startEditing?.()
objectFormRef?.current?.startEditing?.()
@ -423,7 +427,7 @@ const PurchaseOrderInfo = () => {
<PostPurchaseOrder
onOk={() => {
setPostPurchaseOrderOpen(false)
objectFormRef?.current.handleFetchObject()
actions.reload()
}}
objectData={objectFormState.objectData}
/>
@ -441,7 +445,7 @@ const PurchaseOrderInfo = () => {
<AcknowledgePurchaseOrder
onOk={() => {
setAcknowledgePurchaseOrderOpen(false)
objectFormRef?.current.handleFetchObject()
actions.reload()
}}
objectData={objectFormState.objectData}
/>
@ -459,7 +463,7 @@ const PurchaseOrderInfo = () => {
<CancelPurchaseOrder
onOk={() => {
setCancelPurchaseOrderOpen(false)
objectFormRef?.current.handleFetchObject()
actions.reload()
}}
objectData={objectFormState.objectData}
/>

View File

@ -5,12 +5,10 @@ import ObjectTable from '../common/ObjectTable'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTableViewButton from '../common/ObjectTableViewButton'
import FilterSidebarButton from '../common/FilterSidebarButton'
import GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode'
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
import ColumnViewButton from '../common/ColumnViewButton'
import ExportListButton from '../common/ExportListButton'
const Shipments = () => {
const [newShipmentOpen, setNewShipmentOpen] = useState(false)
@ -21,9 +19,6 @@ const Shipments = () => {
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('shipments')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('Shipments')
const actionItems = {
items: [
{
@ -49,7 +44,7 @@ const Shipments = () => {
return (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex vertical={'true'} gap='large'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
@ -61,16 +56,13 @@ const Shipments = () => {
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='shipment' />
</Space>
<Space>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
@ -79,8 +71,6 @@ const Shipments = () => {
visibleColumns={columnVisibility}
type='shipment'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
expandHeight={true}
/>
</Flex>
<Modal

View File

@ -64,6 +64,10 @@ const PurchaseOrderInfo = () => {
})
const actions = {
reload: () => {
objectFormRef?.current?.handleFetchObject?.()
return true
},
edit: () => {
orderItemsTableRef?.current?.startEditing?.()
objectFormRef?.current?.startEditing?.()
@ -328,7 +332,7 @@ const PurchaseOrderInfo = () => {
<PostPurchaseOrder
onOk={() => {
setPostPurchaseOrderOpen(false)
objectFormRef?.current.handleFetchObject()
actions.reload()
}}
objectData={objectFormState.objectData}
/>
@ -346,7 +350,7 @@ const PurchaseOrderInfo = () => {
<AcknowledgePurchaseOrder
onOk={() => {
setAcknowledgePurchaseOrderOpen(false)
objectFormRef?.current.handleFetchObject()
actions.reload()
}}
objectData={objectFormState.objectData}
/>

View File

@ -57,6 +57,10 @@ const ShipmentInfo = () => {
const [receiveShipmentOpen, setReceiveShipmentOpen] = useState(false)
const [cancelShipmentOpen, setCancelShipmentOpen] = useState(false)
const actions = {
reload: () => {
objectFormRef?.current?.handleFetchObject?.()
return true
},
edit: () => {
objectFormRef?.current?.startEditing?.()
return false
@ -257,7 +261,7 @@ const ShipmentInfo = () => {
<ShipShipment
onOk={() => {
setShipShipmentOpen(false)
objectFormRef?.current.handleFetchObject()
actions.reload()
}}
objectData={objectFormState.objectData}
/>
@ -275,7 +279,7 @@ const ShipmentInfo = () => {
<ReceiveShipment
onOk={() => {
setReceiveShipmentOpen(false)
objectFormRef?.current.handleFetchObject()
actions.reload()
}}
objectData={objectFormState.objectData}
/>
@ -293,7 +297,7 @@ const ShipmentInfo = () => {
<CancelShipment
onOk={() => {
setCancelShipmentOpen(false)
objectFormRef?.current.handleFetchObject()
actions.reload()
}}
objectData={objectFormState.objectData}
/>

View File

@ -9,12 +9,10 @@ import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTable from '../common/ObjectTable'
import ObjectTableViewButton from '../common/ObjectTableViewButton'
import FilterSidebarButton from '../common/FilterSidebarButton'
import ListIcon from '../../Icons/ListIcon'
import GridIcon from '../../Icons/GridIcon'
import useViewMode from '../hooks/useViewMode'
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
import ColumnViewButton from '../common/ColumnViewButton'
import ExportListButton from '../common/ExportListButton'
const StockAudits = () => {
const tableRef = useRef()
@ -26,9 +24,6 @@ const StockAudits = () => {
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('stockAudits')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('StockAudits')
const actionItems = {
items: [
{
@ -54,7 +49,7 @@ const StockAudits = () => {
return (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex vertical={'true'} gap='large'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
@ -66,16 +61,13 @@ const StockAudits = () => {
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='stockAudit' />
</Space>
<Space>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
@ -85,8 +77,6 @@ const StockAudits = () => {
visibleColumns={columnVisibility}
type='stockAudit'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
expandHeight={true}
/>
</Flex>
<Modal

View File

@ -36,7 +36,6 @@ const NewStockAudit = ({ onOk, reset, defaultValues }) => {
bordered={false}
visibleProperties={{
_id: false,
_reference: false,
createdAt: false,
updatedAt: false
}}

View File

@ -51,6 +51,10 @@ const StockAuditInfo = () => {
})
const actions = {
reload: () => {
objectFormRef?.current.handleFetchObject()
return true
},
edit: () => {
objectFormRef?.current.startEditing()
return false

View File

@ -2,12 +2,10 @@ import { useRef } from 'react'
import { Button, Flex, Space, Dropdown } from 'antd'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTable from '../common/ObjectTable'
import ObjectTableViewButton from '../common/ObjectTableViewButton'
import FilterSidebarButton from '../common/FilterSidebarButton'
import GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode'
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
import ColumnViewButton from '../common/ColumnViewButton'
import ExportListButton from '../common/ExportListButton'
import ReloadIcon from '../../Icons/ReloadIcon'
const StockEvents = () => {
@ -15,10 +13,7 @@ const StockEvents = () => {
const [viewMode, setViewMode] = useViewMode('stockEvents')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('stockEvent')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('StockEvents')
useColumnVisibility('stockEvents')
const actionItems = {
items: [
@ -37,7 +32,7 @@ const StockEvents = () => {
return (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex vertical={'true'} gap='large'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
@ -49,16 +44,13 @@ const StockEvents = () => {
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='stockEvent' />
</Space>
<Space>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
@ -68,8 +60,6 @@ const StockEvents = () => {
visibleColumns={columnVisibility}
type='stockEvent'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
expandHeight={true}
/>
</Flex>
</>

View File

@ -1,112 +0,0 @@
import { useState, useRef } from 'react'
import { Button, Flex, Space, Modal, Dropdown } from 'antd'
import NewStockLocation from './StockLocations/NewStockLocation'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTable from '../common/ObjectTable'
import ObjectTableViewButton from '../common/ObjectTableViewButton'
import FilterSidebarButton from '../common/FilterSidebarButton'
import useViewMode from '../hooks/useViewMode'
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
import ColumnViewButton from '../common/ColumnViewButton'
import ExportListButton from '../common/ExportListButton'
const StockLocations = () => {
const tableRef = useRef()
const [newOpen, setNewOpen] = useState(false)
const [viewMode, setViewMode] = useViewMode('stockLocations')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('stockLocation')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('StockLocations')
const actionItems = {
items: [
{
label: 'New Stock Location',
key: 'new',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'new') {
setNewOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='stockLocation'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='stockLocation' />
</Space>
<Space>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='stockLocation'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
expandHeight={true}
/>
</Flex>
<Modal
open={newOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={640}
onCancel={() => {
setNewOpen(false)
}}
destroyOnHidden={true}
>
<NewStockLocation
onOk={() => {
setNewOpen(false)
tableRef.current?.reload()
}}
reset={newOpen}
/>
</Modal>
</>
)
}
export default StockLocations

View File

@ -1,69 +0,0 @@
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewStockLocation = ({ onOk, reset }) => {
return (
<NewObjectForm type={'stockLocation'} reset={reset} defaultValues={{}}>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Details',
key: 'details',
content: (
<ObjectInfo
type='stockLocation'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='stockLocation'
column={1}
bordered={false}
visibleProperties={{
_id: false,
_reference: false,
createdAt: false,
updatedAt: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Stock Location'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewStockLocation.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool
}
export default NewStockLocation

View File

@ -1,208 +0,0 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import loglevel from 'loglevel'
import config from '../../../../config.js'
import useCollapseState from '../../hooks/useCollapseState.jsx'
import NotesPanel from '../../common/NotesPanel.jsx'
import InfoCollapse from '../../common/InfoCollapse.jsx'
import ObjectInfo from '../../common/ObjectInfo.jsx'
import ViewButton from '../../common/ViewButton.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import ObjectForm from '../../common/ObjectForm.jsx'
import EditButtons from '../../common/EditButtons.jsx'
import LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('StockLocationInfo')
log.setLevel(config.logLevel)
const StockLocationInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const stockLocationId = new URLSearchParams(location.search).get(
'stockLocationId'
)
const [collapseState, updateCollapseState] = useCollapseState(
'StockLocationInfo',
{
info: true,
notes: true,
auditLogs: true
}
)
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
locked: false,
loading: false,
objectData: {}
})
const actions = {
edit: () => {
objectFormRef?.current.startEditing()
return false
},
cancelEdit: () => {
objectFormRef?.current.cancelEditing()
return true
},
finishEdit: () => {
objectFormRef?.current.handleUpdate()
return true
}
}
return (
<>
<Flex
gap='large'
vertical='true'
style={{
maxHeight: '100%',
minHeight: 0
}}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='stockLocation'
id={stockLocationId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Stock Location' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<UserNotifierToggle
type='stockLocation'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
<DocumentPrintButton
type='stockLocation'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
actionHandlerRef.current.callAction('finishEdit')
}}
cancelEditing={() => {
actionHandlerRef.current.callAction('cancelEdit')
}}
startEditing={() => {
actionHandlerRef.current.callAction('edit')
}}
editLoading={objectFormState.editLoading}
formValid={objectFormState.formValid}
disabled={objectFormState.lock?.locked || objectFormState.loading}
loading={objectFormState.editLoading}
/>
</Space>
</Flex>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<InfoCollapse
title='Stock Location'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<ObjectForm
id={stockLocationId}
type='stockLocation'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => {
return (
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='stockLocation'
objectData={objectData}
labelWidth='175px'
/>
)
}}
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={stockLocationId} type='stockLocation' />
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{ 'parent._id': stockLocationId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
</>
)
}
export default StockLocationInfo

View File

@ -1,112 +0,0 @@
import { useState, useRef } from 'react'
import { Button, Flex, Space, Modal, Dropdown } from 'antd'
import NewStockTransfer from './StockTransfers/NewStockTransfer'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTable from '../common/ObjectTable'
import ObjectTableViewButton from '../common/ObjectTableViewButton'
import FilterSidebarButton from '../common/FilterSidebarButton'
import useViewMode from '../hooks/useViewMode'
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
import ColumnViewButton from '../common/ColumnViewButton'
import ExportListButton from '../common/ExportListButton'
const StockTransfers = () => {
const tableRef = useRef()
const [newOpen, setNewOpen] = useState(false)
const [viewMode, setViewMode] = useViewMode('stockTransfers')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('stockTransfer')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('StockTransfers')
const actionItems = {
items: [
{
label: 'New Stock Transfer',
key: 'new',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'new') {
setNewOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='stockTransfer'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='stockTransfer' />
</Space>
<Space>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='stockTransfer'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
expandHeight={true}
/>
</Flex>
<Modal
open={newOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={740}
onCancel={() => {
setNewOpen(false)
}}
destroyOnHidden={true}
>
<NewStockTransfer
onOk={() => {
setNewOpen(false)
tableRef.current?.reload()
}}
reset={newOpen}
/>
</Modal>
</>
)
}
export default StockTransfers

View File

@ -1,85 +0,0 @@
import PropTypes from 'prop-types'
import dayjs from 'dayjs'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const defaultTransferName = () =>
`Transfer ${dayjs().format('YYYY-MM-DD HH:mm:ss')}`
const NewStockTransfer = ({ onOk, reset }) => {
return (
<NewObjectForm
type={'stockTransfer'}
reset={reset}
defaultValues={{
name: defaultTransferName(),
state: { type: 'draft' },
lines: []
}}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='stockTransfer'
column={1}
bordered={false}
labelWidth={80}
isEditing={true}
required={true}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='stockTransfer'
column={1}
bordered={false}
labelWidth={80}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false,
lines: false,
_reference: false,
postedAt: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Stock Transfer'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewStockTransfer.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool
}
export default NewStockTransfer

View File

@ -1,46 +0,0 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const PostStockTransfer = ({ onOk, objectData }) => {
const [postLoading, setPostLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handlePost = async () => {
setPostLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'StockTransfer',
'post'
)
if (result) {
message.success('Stock transfer posted')
onOk(result)
}
} catch (error) {
console.error('Error posting stock transfer:', error)
} finally {
setPostLoading(false)
}
}
return (
<MessageDialogView
title={'Post this stock transfer?'}
description={`Receiving will move stock to the target locations, create destination stock rows, record stock events owned by this transfer, and fill in the "to" stock on each line.`}
onOk={handlePost}
okText='Post'
okLoading={postLoading}
/>
)
}
PostStockTransfer.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default PostStockTransfer

View File

@ -1,265 +0,0 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card, Modal } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import useCollapseState from '../../hooks/useCollapseState.jsx'
import NotesPanel from '../../common/NotesPanel.jsx'
import InfoCollapse from '../../common/InfoCollapse.jsx'
import ObjectInfo from '../../common/ObjectInfo.jsx'
import ObjectProperty from '../../common/ObjectProperty.jsx'
import ViewButton from '../../common/ViewButton.jsx'
import { getModelProperty, getModelByName } from '../../../../database/ObjectModels.js'
import PostStockTransfer from './PostStockTransfer.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import StockTransferIcon from '../../../Icons/StockTransferIcon.jsx'
import ObjectForm from '../../common/ObjectForm.jsx'
import EditButtons from '../../common/EditButtons.jsx'
import LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const StockTransferInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const stockTransferId = new URLSearchParams(location.search).get(
'stockTransferId'
)
const [postOpen, setPostOpen] = useState(false)
const [collapseState, updateCollapseState] = useCollapseState(
'StockTransferInfo',
{
info: true,
lines: true,
notes: true,
auditLogs: true
}
)
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
locked: false,
loading: false,
objectData: {}
})
const actions = {
edit: () => {
objectFormRef?.current.startEditing()
return false
},
cancelEdit: () => {
objectFormRef?.current.cancelEditing()
return true
},
finishEdit: () => {
objectFormRef?.current.handleUpdate()
return true
},
delete: () => {
objectFormRef?.current.handleDelete?.()
return true
},
post: () => {
setPostOpen(true)
return true
}
}
const editDisabled =
getModelByName('stockTransfer')
?.actions?.find((action) => action.name === 'edit')
?.disabled(objectFormState.objectData) ?? false
return (
<>
<Flex
gap='large'
vertical='true'
style={{
maxHeight: '100%',
minHeight: 0
}}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='stockTransfer'
id={stockTransferId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Transfer' },
{ key: 'lines', label: 'Lines' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<UserNotifierToggle
type='stockTransfer'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
<DocumentPrintButton
type='stockTransfer'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
actionHandlerRef.current.callAction('finishEdit')
}}
cancelEditing={() => {
actionHandlerRef.current.callAction('cancelEdit')
}}
startEditing={() => {
actionHandlerRef.current.callAction('edit')
}}
editLoading={objectFormState.editLoading}
formValid={objectFormState.formValid}
disabled={
objectFormState.lock?.locked ||
objectFormState.loading ||
editDisabled
}
loading={objectFormState.editLoading}
/>
</Space>
</Flex>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<ObjectForm
id={stockTransferId}
type='stockTransfer'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<Flex vertical gap={'large'}>
<InfoCollapse
title='Stock transfer'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='stockTransfer'
objectData={objectData}
labelWidth='175px'
visibleProperties={{ lines: false }}
/>
</InfoCollapse>
<InfoCollapse
title='Lines'
icon={<StockTransferIcon />}
active={collapseState.lines}
onToggle={(expanded) =>
updateCollapseState('lines', expanded)
}
collapseKey='lines'
>
<ObjectProperty
{...getModelProperty('stockTransfer', 'lines')}
isEditing={isEditing}
objectData={objectData}
loading={loading}
size='medium'
/>
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={stockTransferId} type='stockTransfer' />
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{ 'parent._id': stockTransferId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
<Modal
open={postOpen}
onCancel={() => {
setPostOpen(false)
}}
width={520}
footer={null}
destroyOnHidden={true}
centered={true}
>
<PostStockTransfer
onOk={() => {
setPostOpen(false)
objectFormRef?.current.handleFetchObject()
}}
objectData={objectFormState.objectData}
/>
</Modal>
</>
)
}
export default StockTransferInfo

View File

@ -1,193 +0,0 @@
import { useContext, useEffect, useState } from 'react'
import {
Flex,
Typography,
Button,
Dropdown,
Skeleton,
Tag,
Divider
} from 'antd'
import useCollapseState from '../hooks/useCollapseState'
import InfoCollapse from '../common/InfoCollapse'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import DownloadIcon from '../../Icons/DownloadIcon'
import DeveloperIcon from '../../Icons/DeveloperIcon'
import { version as appVersion } from '../../../../package.json'
import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
import { ElectronContext } from '../context/ElectronContext'
import { AppUpdateContext } from '../context/AppUpdateContext'
import { useMediaQuery } from 'react-responsive'
import FarmControlAppIcon from '../../Logos/FarmControlAppIcon'
const { Title, Text, Link } = Typography
const About = () => {
const [collapseState, updateCollapseState] = useCollapseState('About', {
updater: true
})
const { token } = useContext(AuthContext)
const { fetchApiServerVersion, fetchWsServerVersion } =
useContext(ApiServerContext)
const { isElectron, getElectronVersion } = useContext(ElectronContext)
const { checkForUpdates } = useContext(AppUpdateContext)
const isMobile = useMediaQuery({ maxWidth: 768 })
const buildNumber = import.meta.env.VITE_BUILD_NUMBER
? 'b' + import.meta.env.VITE_BUILD_NUMBER
: 'dev'
const developmentMode = import.meta.env.MODE === 'development'
const actions = [
{
label: 'Reload Window',
icon: <ReloadIcon />,
onClick: () => {
window.location.reload()
}
}
]
if (isElectron) {
actions.unshift(
{
label: 'Check for Updates',
icon: <DownloadIcon />,
onClick: checkForUpdates
},
{ type: 'divider' }
)
}
useEffect(() => {
if (token) {
fetchApiServerVersion().then((version) => {
setApiServerVersion(version)
})
fetchWsServerVersion().then((version) => {
setWsServerVersion(version)
})
}
}, [fetchApiServerVersion, fetchWsServerVersion, token])
useEffect(() => {
if (!isElectron) return
getElectronVersion()
.then((version) => {
setElectronVersion(version || 'unknown')
})
.catch(() => {
setElectronVersion('unknown')
})
}, [getElectronVersion, isElectron])
const [apiServerVersion, setApiServerVersion] = useState(null)
const [wsServerVersion, setWsServerVersion] = useState(null)
const [electronVersion, setElectronVersion] = useState(null)
const apiServerVersionText = apiServerVersion ? (
<Text>
{`v${apiServerVersion.version}-${apiServerVersion.buildNumber == 'dev' ? 'dev' : 'b' + apiServerVersion.buildNumber}`}
</Text>
) : (
<Skeleton.Input active size='small' className='text-skeleton' />
)
const wsServerVersionText = wsServerVersion ? (
<Text>{`v${wsServerVersion.version}-${wsServerVersion.buildNumber == 'dev' ? 'dev' : 'b' + wsServerVersion.buildNumber}`}</Text>
) : (
<Skeleton.Input active size='small' className='text-skeleton' />
)
const electronVersionText = electronVersion ? (
<Text>{`v${electronVersion}`}</Text>
) : (
<Skeleton.Input active size='small' className='text-skeleton' />
)
return (
<div style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
<Flex vertical gap='large'>
<Flex vertical gap='middle' align='start'>
<Dropdown menu={{ items: actions }}>
<Button>Actions</Button>
</Dropdown>
</Flex>
<Flex vertical gap='large'>
<InfoCollapse
title='About Farm Control'
icon={<InfoCircleIcon />}
canCollapse={false}
active={collapseState.purchaseOrderStats}
onToggle={(isActive) =>
updateCollapseState('purchaseOrderStats', isActive)
}
className='no-t-padding-collapse'
collapseKey='purchaseOrderStats'
>
<Flex gap='large'>
<FarmControlAppIcon
style={{ width: '200px', height: '200px' }}
/>
<Flex vertical gap='small' justify='center'>
<Flex gap='middle' align='center'>
<Title level={2} style={{ margin: 0 }}>
Farm Control
</Title>
{developmentMode && !isMobile && (
<Tag
color='yellow'
style={{ marginRight: 0 }}
icon={<DeveloperIcon />}
>
Development
</Tag>
)}
</Flex>
<Text type='secondary'>
3D Printer ERP and Control Software.
</Text>
<Flex style={{ columnGap: '15px', rowGap: '8px' }} wrap='wrap'>
<Text type='secondary'>
User Interface:{' '}
<Text>
v{appVersion}-{buildNumber}
</Text>
</Text>
{isElectron && (
<Text type='secondary'>
Electron: {electronVersionText}
</Text>
)}
<Text type='secondary'>REST API: {apiServerVersionText}</Text>
<Text type='secondary'>
Web Socket: {wsServerVersionText}
</Text>
</Flex>
<Flex gap='middle' align='center'>
{developmentMode && isMobile && (
<Tag
color='yellow'
style={{ marginRight: 0 }}
icon={<DeveloperIcon />}
>
Development
</Tag>
)}
<Link href='https://ci.tombutcher.work/job/farmcontrol'>
Jenkins
</Link>
<Divider type='vertical' style={{ margin: 0 }} />
<Link href='https://github.com/farmcontrol'>GitHub</Link>
</Flex>
</Flex>
</Flex>
</InfoCollapse>
</Flex>
</Flex>
</div>
)
}
export default About

View File

@ -1,107 +0,0 @@
import { useRef, useState } from 'react'
import { Button, Flex, Space, Modal, Dropdown } from 'antd'
import NewAppPassword from './AppPasswords/NewAppPassword'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ColumnViewButton from '../common/ColumnViewButton'
import ObjectTable from '../common/ObjectTable'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import ObjectTableViewButton from '../common/ObjectTableViewButton'
import FilterSidebarButton from '../common/FilterSidebarButton'
import useViewMode from '../hooks/useViewMode'
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
import ExportListButton from '../common/ExportListButton'
const AppPasswords = () => {
const [newAppPasswordOpen, setNewAppPasswordOpen] = useState(false)
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('appPassword')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('appPassword')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('AppPasswords')
const actionItems = {
items: [
{
label: 'New App Password',
key: 'newAppPassword',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newAppPassword') {
setNewAppPasswordOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex justify={'space-between'}>
<Space>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='appPassword'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='appPassword' />
</Space>
<Space>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
type='appPassword'
cards={viewMode === 'cards'}
visibleColumns={columnVisibility}
showFilterSidebar={showFilterSidebar}
expandHeight={true}
/>
<Modal
open={newAppPasswordOpen}
footer={null}
width={700}
onCancel={() => {
setNewAppPasswordOpen(false)
}}
>
<NewAppPassword
onOk={() => {
setNewAppPasswordOpen(false)
tableRef.current?.reload()
}}
reset={newAppPasswordOpen}
/>
</Modal>
</Flex>
</>
)
}
export default AppPasswords

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