feat: default document language select

This commit is contained in:
Joshua Shoemaker 2023-02-20 21:47:14 -06:00
parent 290bc8481d
commit 8eada10e87
28 changed files with 648 additions and 176 deletions

View File

@ -21,3 +21,24 @@ func GetInstance() *App {
func (a *App) Startup(ctx context.Context) {
a.Context = ctx
}
type Language struct {
DisplayName string
ProcessCode string
TranslateCode string
}
func GetSuppportedLanguages() []Language {
return []Language{
{
DisplayName: "English",
ProcessCode: "eng",
TranslateCode: "en",
},
{
DisplayName: "Hebrew",
ProcessCode: "heb",
TranslateCode: "he",
},
}
}

View File

@ -1,5 +1,7 @@
package document
import app "textualize/core/App"
type Entity struct {
Id string
GroupId string
@ -7,6 +9,7 @@ type Entity struct {
Path string
ProjectId string
Areas []Area
DefaultLanguage app.Language
}
type Area struct {
@ -16,6 +19,7 @@ type Area struct {
StartY int
EndX int
EndY int
Language app.Language
}
func (e *Entity) AddArea(a Area) {

View File

@ -1,6 +1,18 @@
package session
import (
app "textualize/core/App"
)
type Project struct {
Id string
OrganizationId string
Name string
Settings ProjectSettings
}
type ProjectSettings struct {
DefaultProcessLanguage app.Language
DefaultTranslateTargetLanguage app.Language
IsHosted bool
}

View File

@ -3,7 +3,8 @@
"rules": {
"@next/next/no-img-element": "off",
"quotes": ["warn", "single"],
"semi": ["warn", "never"]
"semi": ["warn", "never"],
"react-hooks/exhaustive-deps": "off"
},
"ignorePatterns": ["wailsjs/*"]
}

View File

@ -1,4 +1,7 @@
import { useRef } from 'react'
import { Switch } from '@headlessui/react'
import { useRef, useState } from 'react'
import classNames from '../../utils/classNames'
type Props = {
onCreateNewProjectHandler: (projectName: string) => void
@ -6,16 +9,17 @@ type Props = {
const NewProjectModal = (props: Props) => {
const projectNameRef = useRef<HTMLInputElement>(null)
const [isHostedProjectSelected, setIsHostedProjectSelected] = useState(false)
return (
<div className=" p-8 absolute top-2/4 -translate-y-1/2 left-2/4 -translate-x-1/2 z-50 bg-white shadow sm:rounded-lg">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg font-medium leading-6 text-gray-900">Name Your New Project</h3>
<div className="mt-2 max-w-xl text-sm text-gray-500">
<p>A unique name will be best. This <b>can</b> be changed later.</p>
<p>A unique name makes the project easier to identify later. This <b>can</b> be changed later.</p>
</div>
<form className="mt-5 sm:flex sm:items-center">
<div className="w-full sm:max-w-xs">
<form className="mt-3 items-center">
<div className="w-full mb-5">
<label htmlFor="name" className="sr-only">
Project Name
</label>
@ -25,17 +29,46 @@ const NewProjectModal = (props: Props) => {
id="name"
autoFocus
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm text-gray-900"
placeholder="New Project"
placeholder="Project Name"
ref={projectNameRef}
/>
</div>
<Switch.Group as="div" className="flex items-center justify-between mb-5">
<span className="flex flex-grow flex-col">
<Switch.Label as="span" className="text-sm font-medium text-gray-900" passive>
Hosted Project
</Switch.Label>
<Switch.Description as="span" className="text-sm text-gray-500">
A hosted project can store and process data on a server. This <b>can</b> be added later. <em className='text-xs'>Currently unavalible</em>
</Switch.Description>
</span>
<Switch
checked={isHostedProjectSelected}
onChange={setIsHostedProjectSelected}
disabled
className={classNames(
isHostedProjectSelected ? 'bg-indigo-600' : 'bg-gray-200',
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2'
)}
>
<span
aria-hidden="true"
className={classNames(
isHostedProjectSelected ? 'translate-x-5' : 'translate-x-0',
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out'
)}
/>
</Switch>
</Switch.Group>
<button
type="submit"
onClick={() => {
if (!projectNameRef.current?.value) return
props.onCreateNewProjectHandler(projectNameRef.current?.value)
}}
className="mt-3 inline-flex w-full items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
className="inline-flex w-full items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:mt-0 sm:w-auto sm:text-sm"
>
Start
</button>

View File

@ -5,10 +5,7 @@ import { Fragment } from 'react'
import { useNavigation } from '../../context/Navigation/provider'
import { mainPages } from '../../context/Navigation/types'
import UserAvatar from './UserAvatar'
function classNames(...classes: any[]) {
return classes.filter(Boolean).join(' ')
}
import classNames from '../../utils/classNames'
const Search = () => {
const { setSelectedMainPage } = useNavigation()
@ -75,7 +72,7 @@ const Search = () => {
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<Menu.Items className="absolute right-0 z-10 mt-1 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{userNavigation.map((item) => (
<Menu.Item key={item.name}>
{({ active }) => (

View File

@ -4,6 +4,7 @@ import React, { useEffect, useRef } from 'react'
import { useProject } from '../../context/Project/provider'
import loadImage from '../../useCases/loadImage'
import processImageArea from '../../useCases/processImageArea'
import LanguageSelect from './LanguageSelect'
const DocumentRenderer = () => {
const { getSelectedDocument, requestAddArea } = useProject()
@ -146,13 +147,17 @@ const DocumentRenderer = () => {
useEffect(() => {
if (selectedDocument?.path) applyDocumentToCanvas(selectedDocument.path)
}, [selectedDocument?.id])
useEffect(() => {
applyAreasToCanvas()
}, [areas, areas?.length])
})
return <div className="relative">
return <div className='relative'>
<div className='flex justify-between mt-2'>
<h1 className="text-2xl font-semibold text-gray-900">
{getSelectedDocument()?.name}
</h1>
<LanguageSelect shouldUpdateDocument defaultLanguage={selectedDocument?.defaultLanguage} />
</div>
<div className="relative mt-2">
<canvas
className="absolute border-4 border-dashed border-gray-200"
ref={documentCanvas}
@ -169,6 +174,7 @@ const DocumentRenderer = () => {
onMouseMove={handleMouseMove}
/>
</div>
</div>
}
export default DocumentRenderer

View File

@ -0,0 +1,87 @@
import { Combobox } from '@headlessui/react'
import { LanguageIcon } from '@heroicons/react/20/solid'
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/24/outline'
import { useEffect, useState } from 'react'
import { useProject } from '../../context/Project/provider'
import classNames from '../../utils/classNames'
import getSupportedLanguages from '../../utils/getSupportedLanguages'
import { ipc } from '../../wailsjs/wailsjs/go/models'
type forAreaType = { shouldUpdateArea?: true, shouldUpdateDocument?: never }
type forDocumentType = { shouldUpdateDocument?: true, shouldUpdateArea?: never }
type Props = (forAreaType | forDocumentType) & { defaultLanguage?: ipc.Language }
const LanguageSelect = (props?: Props) => {
const { requestUpdateDocument, getSelectedDocument } = useProject()
const [languages, setLanguages] = useState<ipc.Language[]>([])
const [query, setQuery] = useState('')
const [selectedLanguage, setSelectedLanguage] = useState<ipc.Language | undefined>(props?.defaultLanguage)
const filteredLanguages = query === ''
? languages
: languages.filter(l => {
return l.displayName.toLowerCase().includes(query.toLowerCase())
})
useEffect(() => {
if (languages.length === 0) {
getSupportedLanguages().then(response => {
setLanguages(response)
})
}
})
useEffect(() => {
if (props?.shouldUpdateDocument && selectedLanguage?.displayName) {
const currentDocument = { ...getSelectedDocument() }
currentDocument.defaultLanguage = selectedLanguage
requestUpdateDocument(currentDocument)
}
}, [selectedLanguage])
return <Combobox as="div" value={selectedLanguage} onChange={setSelectedLanguage}>
<div className="inline-block relative">
<Combobox.Input
className="w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm"
onChange={(event) => setQuery(event.target.value)}
displayValue={(language: ipc.Language) => language?.displayName}
placeholder='Document Language'
/>
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none">
<LanguageIcon className="h-5 w-5 text-gray-400" />
<ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</Combobox.Button>
{filteredLanguages.length > 0 && (
<Combobox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{filteredLanguages.map((l) => (
<Combobox.Option
key={l.displayName}
value={l}
className={({ active }) => classNames(
'relative cursor-default select-none py-2 pl-3 pr-9',
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
)}>
{({ active, selected }) => <>
<span className={classNames('block truncate', selected && 'font-semibold')}>{l.displayName}</span>
{selected && (
<span className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600'
)}>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
)}
</>
}
</Combobox.Option>
))}
</Combobox.Options>
)}
</div>
</Combobox>
}
export default LanguageSelect

View File

@ -22,9 +22,9 @@ const renderSelectedWorkSpace = () => {
<div className="mx-auto px-4 sm:px-6 md:px-8">
<div className="py-1">
<div className="mx-auto px-4 sm:px-6 md:px-8">
<h1 className="text-2xl font-semibold text-gray-900">
{getSelectedDocument()?.name || 'Image Processor'}
</h1>
{!getSelectedDocument()?.id ? <h1 className="text-2xl mt-2 font-semibold text-gray-900">
Image Processor
</h1> : ''}
</div>
{ renderSelectedWorkSpace() }
</div>

View File

@ -1,9 +1,22 @@
import { DocumentPlusIcon } from '@heroicons/react/24/outline'
import { useProject } from '../../context/Project/provider'
export default function NoSelectedDocument() {
const { requestAddDocument, setSelectedDocumentId } = useProject()
const onAddDocumentClickHandler = async () => {
const documentName = 'Untitled Document'
const response = await requestAddDocument('', documentName)
if (!response.id) return
setSelectedDocumentId(response.id)
}
return (
<button
type="button"
onClick={() => onAddDocumentClickHandler()}
className="relative block w-full rounded-lg border-4 border-dashed border-gray-200 p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
<svg

View File

@ -4,6 +4,7 @@ import React, { useRef, useState } from 'react'
import { PlusIcon, XMarkIcon, DocumentPlusIcon } from '@heroicons/react/20/solid'
import { ipc } from '../../wailsjs/wailsjs/go/models'
import { useProject } from '../../context/Project/provider'
import classNames from '../../utils/classNames'
type GroupNavigationItem = {
id: string,
@ -43,29 +44,28 @@ const getNavigationProps = (documents: ipc.Document[], groups: ipc.Group[]): Gro
return [
...groupsWithDocuments,
{
id: 'Uncategorized',
id: '',
name: 'Uncategorized',
documents: documentsWithoutGroup
}
]
}
function classNames(...classes: any[]) {
return classes.filter(Boolean).join(' ')
}
function Sidebar() {
const [selectedGroupId, setSelectedGroupId] = useState('')
const [isAddNewDocumentInputShowing, setIsAddNewDocumentInputShowing] = useState(false)
const [isEditDocumentNameInputShowing, setIsEditDocumentNameInputShowing] = useState(false)
const [isAddNewGroupInputShowing, setIsAddNewGroupInputShowing] = useState(false)
const [isEditAreaNameInputShowing, setIsEditAreaNameInputShowing] = useState(false)
const addDocumentTextInput = useRef<HTMLInputElement>(null)
const addGroupTextInput = useRef<HTMLInputElement>(null)
const editAreaNameTextInput = useRef<HTMLInputElement>(null)
const editDocumentNameTextInput = useRef<HTMLInputElement>(null)
const {
documents,
groups,
getSelectedDocument,
getAreaById,
requestUpdateArea,
requestAddDocument,
@ -75,6 +75,7 @@ function Sidebar() {
selectedDocumentId,
setSelectedDocumentId,
currentSession,
requestUpdateDocument,
} = useProject()
const navigation = getNavigationProps(documents, groups)
@ -115,7 +116,7 @@ function Sidebar() {
setSelectedAreaId(areaId)
}
const onAreaDoubleclick = (areaId: string) => {
const onAreaDoubleClick = (areaId: string) => {
const documentIdOfArea = getDocumentIdFromAreaId(areaId)
setIsEditAreaNameInputShowing(true)
console.log('double click')
@ -132,6 +133,14 @@ function Sidebar() {
setIsAddNewGroupInputShowing(false)
}
const onDocumentDoubleClickHandler = (docuemntId: string) => {
setIsEditDocumentNameInputShowing(true)
}
const onDocumentInputBlur = () => {
setIsEditDocumentNameInputShowing(false)
}
const onCancelAddGroupClickHandler = () => {
setIsAddNewGroupInputShowing(false)
}
@ -153,6 +162,17 @@ function Sidebar() {
setIsEditAreaNameInputShowing(false)
}
const onConfirmDocumentNameChangeHandler = async (documentName: string) => {
const documentToUpdate = { ...getSelectedDocument() }
if (documentToUpdate) {
documentToUpdate.name = documentName
requestUpdateDocument(documentToUpdate)
.then(response => console.log('onConfirmDocumentNameChangeHandler response: ', response))
.catch(console.error)
}
setIsEditDocumentNameInputShowing(false)
}
const onConfirmAddDocumentClickHandler = async (groupId: string) => {
const documentName = addDocumentTextInput.current?.value
if (!documentName) return
@ -294,8 +314,10 @@ function Sidebar() {
{group.documents.map((d, index) => (
<li className='p-0 m-0' key={d.id}>
{!d.areas.length
? <div
?
<div
onClick={() => onDocumentClickHandler(d.id)}
onDoubleClick={() => onDocumentDoubleClickHandler(d.id)}
className={classNames(
d.id === selectedDocumentId
? 'bg-gray-900 text-white'
@ -303,7 +325,22 @@ function Sidebar() {
'group items-center py-2 text-base font-medium rounded-b-md pl-10',
index !== 0 ? 'rounded-t-md' : '',
)}>
<a
{selectedDocumentId === d.id && isEditDocumentNameInputShowing
? <input
type="text"
name="documentName"
id="documentName"
autoFocus
className="h-8 text-white placeholder-gray-400 bg-gray-900 bg-opacity-5 block w-full rounded-none rounded-l-md border-late-700 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
defaultValue={d.name}
onBlur={onDocumentInputBlur}
onKeyDown={(event) => {
onEnterHandler(event,
() => onConfirmDocumentNameChangeHandler(event.currentTarget.value))
}}
ref={editDocumentNameTextInput}
/>
: <a
role='button'
className={classNames(
d.id === selectedDocumentId
@ -314,10 +351,12 @@ function Sidebar() {
>
{d.name}
</a>
}
</div>
: <details>
<summary
onClick={() => onDocumentClickHandler(d.id)}
onDoubleClick={() => onDocumentDoubleClickHandler(d.id)}
className={classNames(
d.id === selectedDocumentId
? 'bg-gray-900 text-white'
@ -326,7 +365,22 @@ function Sidebar() {
index !== 0 ? 'rounded-t-md' : '',
)}>
<a
{selectedDocumentId === d.id && isEditDocumentNameInputShowing
? <input // TODO: this
type="text"
name="documentName"
id="documentName"
autoFocus
className="h-8 text-white placeholder-gray-400 bg-gray-900 bg-opacity-5 block w-full rounded-none rounded-l-md border-late-700 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
defaultValue={d.name}
onBlur={onDocumentInputBlur}
onKeyDown={(event) => {
onEnterHandler(event,
() => onConfirmDocumentNameChangeHandler(event.currentTarget.value))
}}
ref={editDocumentNameTextInput}
/>
: <a
role='button'
className={classNames(
d.id === selectedDocumentId
@ -337,6 +391,7 @@ function Sidebar() {
>
{d.name}
</a>
}
</summary>
<ul>
{d.areas.map((a, index) => (
@ -359,7 +414,7 @@ function Sidebar() {
: <a
role='button'
onClick={() => onAreaClick(a.id)}
onDoubleClick={() => onAreaDoubleclick(a.id)}
onDoubleClick={() => onAreaDoubleClick(a.id)}
className={classNames('text-gray-300 hover:bg-gray-700 hover:text-white',
'group w-full flex items-center pr-2 py-2 text-left font-medium pl-8 text-xs',
'rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 py-2 select-none',

View File

@ -1,4 +1,4 @@
import { DiffEditor } from '@monaco-editor/react'
import { loader, DiffEditor } from '@monaco-editor/react'
import { useEffect, useState } from 'react'
import { useProject } from '../../context/Project/provider'
import type { DiffOnMount } from '@monaco-editor/react/'
@ -6,18 +6,29 @@ import TextEditorButtons from './TextEditorButtons'
import createDiffEditorInteractions from '../../useCases/createDiffEditorInteractions'
import TextPreview from './TextPreview'
import createDebounce from '../../utils/createDebounce'
import LanguageSelect from './LanguageSelect'
loader.config({
paths: {
vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.34.0/min/vs',
},
})
let editorInteractions: ReturnType<typeof createDiffEditorInteractions>
const editorHeightOffset = 234
const TextEditor = () => {
const { selectedDocumentId, getProcessedAreasByDocumentId, requestUpdateDocumentUserMarkdown, getUserMarkdownByDocumentId } = useProject()
const [editorHeight, setEditorHeight] = useState(window.innerHeight - 200)
const { getSelectedDocument, getProcessedAreasByDocumentId, requestUpdateDocumentUserMarkdown, getUserMarkdownByDocumentId } = useProject()
const [editorHeight, setEditorHeight] = useState(window.innerHeight - editorHeightOffset)
const [editorValue, setEditorValue] = useState('')
const [isEditorReady, setIsEditorReady] = useState(false)
const [isPreviewOpen, setIsPreviewOpen] = useState(false)
const [modifiedEditorValue, setModifiedEditorValue] = useState('')
const handleEditorDidMount: DiffOnMount = async (editor, monaco) => {
const selectedDocument = getSelectedDocument()
const selectedDocumentId = selectedDocument?.id || ''
const handleEditorDidMount: DiffOnMount = async (editor, _) => {
const currentDocumentId = selectedDocumentId
editorInteractions = createDiffEditorInteractions(editor)
@ -66,18 +77,23 @@ const TextEditor = () => {
}, [selectedDocumentId, getProcessedAreasByDocumentId])
window.addEventListener('resize', () => {
setEditorHeight(window.innerHeight - 200)
setEditorHeight(window.innerHeight - editorHeightOffset)
})
return <div className='relative m-0 p-0'>
return <div className='m-0 p-0 relative'>
<span className="flex z-0 rounded-md shadow-sm mb-2 mt-2 justify-between">
{isEditorReady
? <TextEditorButtons
? <>
<TextEditorButtons
isPreviewOpen={isPreviewOpen}
togglePreview={() => setIsPreviewOpen(!isPreviewOpen)}
editorInteractions={editorInteractions}
/>
<LanguageSelect shouldUpdateDocument defaultLanguage={selectedDocument?.defaultLanguage} />
</>
: ''
}
</span>
<DiffEditor
original={editorValue}
modified={modifiedEditorValue}
@ -87,6 +103,7 @@ const TextEditor = () => {
options={{
renderMarginRevertIcon: true,
enableSplitViewResizing: false,
glyphMargin: true,
}}
/>

View File

@ -1,10 +1,7 @@
import { ListBulletIcon, MinusIcon } from '@heroicons/react/20/solid'
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'
import createDiffEditorInteractions, { MarkdownOperator } from '../../useCases/createDiffEditorInteractions'
function classNames(...classes: any[]) {
return classes.filter(Boolean).join(' ')
}
import classNames from '../../utils/classNames'
type Props = {
editorInteractions: ReturnType<typeof createDiffEditorInteractions>
@ -14,7 +11,8 @@ type Props = {
const TextEditorButtons = (props: Props) => {
const { editorInteractions, togglePreview } = props
return <span className="isolate inline-flex rounded-md shadow-sm">
return <span className="inline-flex rounded-md shadow-sm">
<button
type="button"
onClick={togglePreview}
@ -143,7 +141,7 @@ const TextEditorButtons = (props: Props) => {
type="button"
onClick={() => editorInteractions.insertMarkdownOperator(MarkdownOperator.DIVIDER)}
className={classNames(
'text-sm relative inline-flex items-center rounded-r-md border',
'text-sm relative inline-flex items-center border',
'border-gray-300 bg-white px-2 py-0 text-gray-700 hover:bg-gray-50',
'focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-1',
'focus:ring-indigo-500 italic',
@ -151,6 +149,8 @@ const TextEditorButtons = (props: Props) => {
<span className="sr-only">Divider</span>
<MinusIcon className="h-5 w-5" aria-hidden="true" />
</button>
</span>
}

View File

@ -4,7 +4,7 @@ type Props = { markdown: string, height: number }
const TextPreview = (props: Props) => (
<div
className='absolute w-[calc(50%-14px)] top-[30px] bg-white overflow-y-scroll p-4 m-0'
className='absolute w-[calc(50%-14px)] top-[46px] bg-white overflow-y-scroll p-4 m-0'
style={{ 'height': `${props.height}px` }}>
<ReactMarkdown
components={{

View File

@ -1,15 +1,20 @@
import { useNavigation } from '../../context/Navigation/provider'
import { workspaces } from '../../context/Navigation/types'
import classNames from '../../utils/classNames'
const tabs = [
{ displayName: 'Processor', type: workspaces.PROCESSOR },
{ displayName: 'Text Editor', type: workspaces.TEXTEDITOR },
{ displayName: 'Translator', type: workspaces.TRANSLATOR },
{ displayName: 'Details', type: workspaces.DETAILS },
{ displayName: 'Translator', type: workspaces.TRANSLATOR, disabled: true, },
{ displayName: 'Details', type: workspaces.DETAILS, disabled: true, },
]
function classNames(...classes: string[]) {
return classes.filter(Boolean).join(' ')
const getTabClasses = (isTabSelected: boolean, isDisabled?: boolean) => {
if (isDisabled) return classNames('cursor-not-allowed w-1/4 py-4 px-1 text-center border-b-2 font-medium text-gray-200 text-sm')
const baseClasses = 'cursor-pointer w-1/4 py-4 px-1 text-center border-b-2 font-medium text-sm'
if (isTabSelected) return classNames(baseClasses, 'border-indigo-500 text-indigo-600')
else return classNames(baseClasses, 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300')
}
export default function ToolTabs() {
@ -24,14 +29,12 @@ export default function ToolTabs() {
<nav className="-mb-px flex" aria-label="Tabs">
{tabs.map((tab) => (
<a
aria-disabled={tab.disabled}
key={tab.displayName}
onClick={_ => setSelectedWorkspace(tab.type)}
className={classNames(
getIsSelectedTab(tab.type)
? 'border-indigo-500 text-indigo-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
'cursor-pointer w-1/4 py-4 px-1 text-center border-b-2 font-medium text-sm'
)}
onClick={_ => {
if (!tab.disabled) setSelectedWorkspace(tab.type)
}}
className={getTabClasses(getIsSelectedTab(tab.type), tab.disabled)}
aria-current={getIsSelectedTab(tab.type) ? 'page' : undefined}
>
{tab.displayName}

View File

@ -23,6 +23,7 @@ const makeDefaultProject = (): ProjectContextType => ({
createNewProject: (name: string) => Promise.resolve(new ipc.Session()),
requestUpdateCurrentUser: (updatedUserProps: UserProps) => Promise.resolve(new ipc.User()),
requestChooseUserAvatar: () => Promise.resolve(''),
requestUpdateDocument: ({}) => Promise.resolve(new ipc.Document),
})
export default makeDefaultProject

View File

@ -7,9 +7,10 @@ import {
RequestAddDocument, RequestAddDocumentGroup, RequestAddProcessedArea,
RequestUpdateArea, RequestUpdateCurrentUser, RequestUpdateDocumentUserMarkdown,
RequestChooseUserAvatar,
RequestUpdateDocument,
} from '../../wailsjs/wailsjs/go/ipc/Channel'
import { ipc } from '../../wailsjs/wailsjs/go/models'
import { AddAreaProps, AreaProps, ProjectContextType, ProjectProps, UserProps } from './types'
import { AddAreaProps, AreaProps, ProjectContextType, ProjectProps, UpdateDocumentRequest, UserProps } from './types'
import makeDefaultProject from './makeDefaultProject'
const ProjectContext = createContext<ProjectContextType>(makeDefaultProject())
@ -122,6 +123,13 @@ export function ProjectProvider({ children, projectProps }: Props) {
return filePathResponse
}
const requestUpdateDocument = async (docuemntProps: UpdateDocumentRequest) => {
console.log('request: ', docuemntProps)
const response = await RequestUpdateDocument(new ipc.Document(docuemntProps))
await updateDocuments()
return response
}
useEffect(() => {
if (!documents.length && !groups.length) updateDocuments()
}, [documents.length, groups.length])
@ -148,6 +156,7 @@ export function ProjectProvider({ children, projectProps }: Props) {
createNewProject,
requestUpdateCurrentUser,
requestChooseUserAvatar,
requestUpdateDocument,
}
return <ProjectContext.Provider value={value}>

View File

@ -26,6 +26,16 @@ export type UserProps = {
email?: string
}
export type UpdateDocumentRequest = {
id?: string,
projectId?: string,
groupId?: string,
name?: string,
path?: string,
areas?: ipc.Area[]
defaultLanguage?: ipc.Language
}
export type ProjectContextType = {
getSelectedDocument: () => ipc.Document | undefined
getAreaById: (areaId: string) => ipc.Area | undefined
@ -45,4 +55,5 @@ export type ProjectContextType = {
createNewProject: (name: string) => Promise<ipc.Session>
requestUpdateCurrentUser: (updatedUserProps: UserProps) => Promise<ipc.User>
requestChooseUserAvatar: () => Promise<string>
requestUpdateDocument: (request: UpdateDocumentRequest) => Promise<ipc.Document>
} & ProjectProps

View File

@ -1,5 +1,4 @@
import { NextPage } from 'next'
import { useEffect } from 'react'
import MainHead from '../components/head'
import MainProject from '../components/project/Main'
import User from '../components/settings/User'
@ -12,12 +11,7 @@ import { useProject } from '../context/Project/provider'
const Home: NextPage = () => {
const { currentSession } = useProject()
const { selectedMainPage, setSelectedMainPage } = useNavigation()
useEffect(() => {
if (!currentSession?.user?.localId) setSelectedMainPage(mainPages.EDITUSER)
else if (!currentSession?.project?.id) setSelectedMainPage(mainPages.SELECTPROJECT)
},)
const { selectedMainPage } = useNavigation()
const renderSelectedMainPage = () => {
if (selectedMainPage === mainPages.SELECTPROJECT) return <MainProject />
@ -31,12 +25,10 @@ const Home: NextPage = () => {
else return <MainProject />
}
return (
<>
return <>
<MainHead />
{renderSelectedMainPage()}
</>
)
}
export default Home

View File

@ -10,7 +10,7 @@ export enum MarkdownOperator {
ITALLICS = '_',
BOLD = '**',
BULLET = '* ',
DIVIDER = '\n\n---\n\n'
DIVIDER = '\n\n---\n'
}
const wrapperOperators = [
@ -43,8 +43,6 @@ const createDiffEditorInteractions = (editor: monaco.editor.IStandaloneDiffEdito
})
if (operator == MarkdownOperator.DIVIDER) {
console.log('lineOfCursor:', lineOfCursor)
console.log('lengthOfLine:', lengthOfLine)
range = {
startLineNumber,
startColumn: lengthOfLine,
@ -72,12 +70,9 @@ const createDiffEditorInteractions = (editor: monaco.editor.IStandaloneDiffEdito
newText = `${operator}${modifiedEditor.getModel()?.getValueInRange(range)}`
}
modifiedEditor.executeEdits('editor', [{
range,
text: newText
}])
modifiedEditor.executeEdits('editor', [{ range, text: newText }])
modifiedEditor.pushUndoStop()
modifiedEditor.focus()
}
}
}

View File

@ -0,0 +1,3 @@
const classNames = (...classes: any[]) => classes.filter(Boolean).join(' ')
export default classNames

View File

@ -0,0 +1,8 @@
import { GetSuppportedLanguages } from '../wailsjs/wailsjs/go/ipc/Channel'
const getSupportedLanguages = async () => {
const response = await GetSuppportedLanguages()
return response
}
export default getSupportedLanguages

View File

@ -14,6 +14,8 @@ export function GetDocuments():Promise<ipc.GetDocumentsResponse>;
export function GetProcessedAreasByDocumentId(arg1:string):Promise<Array<ipc.ProcessedArea>>;
export function GetSuppportedLanguages():Promise<Array<ipc.Language>>;
export function GetUserMarkdownByDocumentId(arg1:string):Promise<ipc.UserMarkdown>;
export function RequestAddArea(arg1:string,arg2:ipc.Area):Promise<ipc.Area>;
@ -30,4 +32,6 @@ export function RequestUpdateArea(arg1:ipc.Area):Promise<ipc.Area>;
export function RequestUpdateCurrentUser(arg1:ipc.User):Promise<ipc.User>;
export function RequestUpdateDocument(arg1:ipc.Document):Promise<ipc.Document>;
export function RequestUpdateDocumentUserMarkdown(arg1:string,arg2:string):Promise<ipc.UserMarkdown>;

View File

@ -26,6 +26,10 @@ export function GetProcessedAreasByDocumentId(arg1) {
return window['go']['ipc']['Channel']['GetProcessedAreasByDocumentId'](arg1);
}
export function GetSuppportedLanguages() {
return window['go']['ipc']['Channel']['GetSuppportedLanguages']();
}
export function GetUserMarkdownByDocumentId(arg1) {
return window['go']['ipc']['Channel']['GetUserMarkdownByDocumentId'](arg1);
}
@ -58,6 +62,10 @@ export function RequestUpdateCurrentUser(arg1) {
return window['go']['ipc']['Channel']['RequestUpdateCurrentUser'](arg1);
}
export function RequestUpdateDocument(arg1) {
return window['go']['ipc']['Channel']['RequestUpdateDocument'](arg1);
}
export function RequestUpdateDocumentUserMarkdown(arg1, arg2) {
return window['go']['ipc']['Channel']['RequestUpdateDocumentUserMarkdown'](arg1, arg2);
}

View File

@ -1,5 +1,21 @@
export namespace ipc {
export class Language {
displayName: string;
processCode: string;
translateCode: string;
static createFrom(source: any = {}) {
return new Language(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.displayName = source["displayName"];
this.processCode = source["processCode"];
this.translateCode = source["translateCode"];
}
}
export class Area {
id: string;
name: string;
@ -7,6 +23,7 @@ export namespace ipc {
startY: number;
endX: number;
endY: number;
language: Language;
static createFrom(source: any = {}) {
return new Area(source);
@ -20,6 +37,25 @@ export namespace ipc {
this.startY = source["startY"];
this.endX = source["endX"];
this.endY = source["endY"];
this.language = this.convertValues(source["language"], Language);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class Document {
@ -29,6 +65,7 @@ export namespace ipc {
path: string;
projectId: string;
areas: Area[];
defaultLanguage: Language;
static createFrom(source: any = {}) {
return new Document(source);
@ -42,6 +79,7 @@ export namespace ipc {
this.path = source["path"];
this.projectId = source["projectId"];
this.areas = this.convertValues(source["areas"], Area);
this.defaultLanguage = this.convertValues(source["defaultLanguage"], Language);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
@ -113,6 +151,7 @@ export namespace ipc {
}
}
export class User {
id: string;
localId: string;
@ -335,9 +374,45 @@ export namespace ipc {
export class ProjectSettings {
defaultProcessLanguage: Language;
defaultTranslateTargetLanguage: Language;
IsHosted: boolean;
static createFrom(source: any = {}) {
return new ProjectSettings(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.defaultProcessLanguage = this.convertValues(source["defaultProcessLanguage"], Language);
this.defaultTranslateTargetLanguage = this.convertValues(source["defaultTranslateTargetLanguage"], Language);
this.IsHosted = source["IsHosted"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class Project {
id: string;
organizationId: string;
name: string;
settings: ProjectSettings;
static createFrom(source: any = {}) {
return new Project(source);
@ -346,9 +421,30 @@ export namespace ipc {
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
this.organizationId = source["organizationId"];
this.name = source["name"];
this.settings = this.convertValues(source["settings"], ProjectSettings);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class Session {
project: Project;
organization: Organization;

View File

@ -1,6 +1,7 @@
package ipc
import (
"fmt"
app "textualize/core/App"
document "textualize/core/Document"
session "textualize/core/Session"
@ -19,7 +20,15 @@ func (c *Channel) GetDocumentById(id string) Document {
var jsonAreas []Area
for _, a := range foundDocument.Areas {
jsonAreas = append(jsonAreas, Area(a))
jsonAreas = append(jsonAreas, Area{
Id: a.Id,
Name: a.Name,
StartX: a.StartX,
StartY: a.StartY,
EndX: a.EndX,
EndY: a.EndY,
Language: Language(a.Language),
})
}
response := Document{
Id: foundDocument.Id,
@ -44,7 +53,15 @@ func (c *Channel) GetDocuments() GetDocumentsResponse {
for _, d := range documents {
jsonAreas := make([]Area, 0)
for _, a := range d.Areas {
jsonAreas = append(jsonAreas, Area(a))
jsonAreas = append(jsonAreas, Area{
Id: a.Id,
Name: a.Name,
StartX: a.StartX,
StartY: a.StartY,
EndX: a.EndX,
EndY: a.EndY,
Language: Language(a.Language),
})
}
jsonDocument := Document{
@ -54,6 +71,7 @@ func (c *Channel) GetDocuments() GetDocumentsResponse {
Path: d.Path,
ProjectId: d.ProjectId,
Areas: jsonAreas,
DefaultLanguage: Language(d.DefaultLanguage),
}
response.Documents = append(response.Documents, jsonDocument)
}
@ -182,9 +200,14 @@ func (c *Channel) RequestAddArea(documentId string, area Area) Area {
EndX: area.EndX,
StartY: area.StartY,
EndY: area.EndY,
Language: app.Language(area.Language),
}
foundDocument.AddArea(newArea)
return Area(newArea)
responseArea := area
responseArea.Id = id
return responseArea
}
func (c *Channel) RequestUpdateArea(updatedArea Area) Area {
@ -206,11 +229,41 @@ func (c *Channel) RequestUpdateArea(updatedArea Area) Area {
}
return Area{
Id: updatedArea.Id,
Name: updatedArea.Name,
StartX: updatedArea.StartX,
StartY: updatedArea.StartY,
EndX: updatedArea.EndX,
EndY: updatedArea.EndY,
Id: areaToUpdate.Id,
Name: areaToUpdate.Name,
StartX: areaToUpdate.StartX,
StartY: areaToUpdate.StartY,
EndX: areaToUpdate.EndX,
EndY: areaToUpdate.EndY,
Language: Language(areaToUpdate.Language),
}
}
func (c *Channel) RequestUpdateDocument(updatedDocument Document) Document {
documentToUpdate := document.GetDocumentCollection().GetDocumentById(updatedDocument.Id)
if documentToUpdate == nil {
return Document{}
}
if updatedDocument.Id != "" {
documentToUpdate.Id = updatedDocument.Id
}
if updatedDocument.Name != "" {
documentToUpdate.Name = updatedDocument.Name
}
if updatedDocument.GroupId != "" {
documentToUpdate.GroupId = updatedDocument.GroupId
}
if updatedDocument.Path != "" {
documentToUpdate.Path = updatedDocument.Path
}
if updatedDocument.DefaultLanguage.DisplayName != "" {
documentToUpdate.DefaultLanguage = app.Language(updatedDocument.DefaultLanguage)
}
fmt.Println("updated doc")
fmt.Println(document.GetDocumentCollection().GetDocumentById(updatedDocument.Id))
return updatedDocument
}

View File

@ -7,6 +7,7 @@ type Document struct {
Path string `json:"path"`
ProjectId string `json:"projectId"`
Areas []Area `json:"areas"`
DefaultLanguage Language `json:"defaultLanguage"`
}
type DocumentCollection struct {
@ -34,6 +35,7 @@ type Area struct {
StartY int `json:"startY"`
EndX int `json:"endX"`
EndY int `json:"endY"`
Language Language `json:"language"`
}
type ProcessedBoundingBox struct {
@ -98,7 +100,15 @@ type Organization struct {
type Project struct {
Id string `json:"id"`
OrganizationId string `json:"organizationId"`
Name string `json:"name"`
Settings ProjectSettings `json:"settings"`
}
type ProjectSettings struct {
DefaultProcessLanguage Language `json:"defaultProcessLanguage"`
DefaultTranslateTargetLanguage Language `json:"defaultTranslateTargetLanguage"`
IsHosted bool `json:"IsHosted"`
}
type Session struct {
@ -106,3 +116,9 @@ type Session struct {
Organization Organization `json:"organization"`
User User `json:"user"`
}
type Language struct {
DisplayName string `json:"displayName"`
ProcessCode string `json:"processCode"`
TranslateCode string `json:"translateCode"`
}

View File

@ -17,8 +17,22 @@ func (c *Channel) GetCurrentSession() Session {
sessionUsers = append(sessionUsers, User(u))
}
currentProject := currentSession.Project
currentDefaultProcessLanguage := Language(currentProject.Settings.DefaultProcessLanguage)
currentDefaultTranslateTargetLanguage := Language(currentProject.Settings.DefaultTranslateTargetLanguage)
project := Project{
Id: currentProject.Id,
Name: currentProject.Name,
OrganizationId: currentProject.OrganizationId,
Settings: ProjectSettings{
DefaultProcessLanguage: currentDefaultProcessLanguage,
DefaultTranslateTargetLanguage: currentDefaultTranslateTargetLanguage,
IsHosted: currentProject.Settings.IsHosted,
},
}
return Session{
Project: Project(currentSession.Project),
Project: Project(project),
User: User(currentSession.User),
Organization: Organization{
Id: currentSession.Organization.Id,
@ -34,6 +48,7 @@ func (c *Channel) CreateNewProject(name string) Session {
currentSession.Project = session.Project{
Id: uuid.NewString(),
OrganizationId: currentSession.Project.OrganizationId,
Name: name,
}
@ -86,3 +101,15 @@ func (c *Channel) RequestChooseUserAvatar() string {
return filePath
}
}
func (c *Channel) GetSuppportedLanguages() []Language {
supportedLanguages := app.GetSuppportedLanguages()
var response []Language
for _, l := range supportedLanguages {
response = append(response, Language(l))
}
return response
}