Board: Design card editor (initial) (#1292)

This commit is contained in:
Anna No 2022-04-07 12:17:43 +07:00 committed by GitHub
parent 167a5c3206
commit 6543fdf4b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 485 additions and 94 deletions

View File

@ -25,7 +25,6 @@ export default mergeIds(boardId, board, {
component: {
CreateBoard: '' as AnyComponent,
CreateCard: '' as AnyComponent,
EditCard: '' as AnyComponent,
KanbanCard: '' as AnyComponent,
CardPresenter: '' as AnyComponent,
TemplatesIcon: '' as AnyComponent,

View File

@ -172,6 +172,9 @@ p:last-child { margin-block-end: 0; }
.justify-end { justify-content: flex-end; }
.items-baseline { align-items: baseline; }
.flex-gap-3 {gap: .75rem;}
.flex-gap-1 {gap: .25rem;}
.flex-presenter, .inline-presenter {
flex-wrap: nowrap;
cursor: pointer;
@ -313,6 +316,9 @@ p:last-child { margin-block-end: 0; }
.pr-4 { padding-right: 1rem; }
.pr-24 { padding-right: 6rem; }
.p-2 { padding: .5rem; }
.p-3 { padding: .75rem; }
.p-6 { padding: 1.5rem; }
.p-10 { padding: 2.5rem; }
/* --------- */
@ -356,6 +362,7 @@ p:last-child { margin-block-end: 0; }
.h-2 { height: .5rem; }
.h-9 { height: 2.25rem; }
.w-full { width: 100%; }
.w-9 { width: 2.25rem; }
.w-85 {width: 21.25rem; }
.w-165 {width: 41.25rem; }
.min-w-0 { min-width: 0; }
@ -416,11 +423,7 @@ a.no-line {
.pointer-events-none { pointer-events: none; }
/* Text */
.text-sm { font-size: .75rem; }
.text-md { font-size: .8125rem; }
.text-lg { font-size: 1.125rem; }
.font-normal { font-weight: 400; }
.font-medium { font-weight: 500; }
.fs-title {
font-weight: 500;
font-size: 1rem;
@ -434,6 +437,11 @@ a.no-line {
color: var(--theme-content-trans-color);
user-select: none;
}
.text-sm { font-size: .75rem; }
.text-md { font-size: .8125rem; }
.text-lg { font-size: 1.125rem; }
.font-normal { font-weight: 400; }
.font-medium { font-weight: 500; }
.fs-bold { font-weight: 500; }
.uppercase { text-transform: uppercase; }
.text-left { text-align: left; }
@ -529,6 +537,8 @@ a.no-line {
.red-color { color: var(--highlight-red); }
.border-radius-3 {border-radius: 0.75rem;}
.border-bg-accent {border: 1px solid var(--theme-bg-accent-color);}
.border-primary-button { border-color: var(--primary-button-border); }
.border-button-enabled { border: 1px solid var(--theme-button-border-enabled); }
.bottom-divider { border-bottom: 1px solid var(--theme-menu-divider); }

View File

@ -24,6 +24,7 @@
export let object: Doc
export let fullSize: boolean = false
export let showCommenInput: boolean = true
export let transparent: boolean = false
let txes: DisplayTx[] = []
@ -39,7 +40,7 @@
const descriptors = createQuery()
$: descriptors.query(activity.class.TxViewlet, {}, (result) => {
viewlets = new Map(result.map((r) => [activityKey(r.objectClass, r.txClass), r]))
editable = new Map(result.map(it => [it.objectClass, it.editable ?? false]))
})
@ -78,9 +79,11 @@
{/if}
</div>
</Scroller>
<div class="ref-input">
<Component is={chunter.component.CommentInput} props={{ object }} />
</div>
{#if showCommenInput}
<div class="ref-input">
<Component is={chunter.component.CommentInput} props={{ object }} />
</div>
{/if}
</div>
{:else}
<Scroller>
@ -105,9 +108,11 @@
</div>
</div>
</Scroller>
<div class="ref-input fill">
<Component is={chunter.component.CommentInput} props={{ object }} />
</div>
{#if showCommenInput}
<div class="ref-input fill">
<Component is={chunter.component.CommentInput} props={{ object }} />
</div>
{/if}
{/if}
<style lang="scss">

View File

@ -20,9 +20,27 @@
"Card": "Card",
"Assignee": "Assignee",
"Description": "Description",
"DescriptionPlaceholder": "Add a more detailed description...",
"Location": "Location",
"Members": "Members",
"BoardCreateLabel": "Board",
"Settings": "Settings"
"Settings": "Settings",
"InList": "in list",
"AddToCard": "Add to card",
"Labels": "Labels",
"Checklist": "Checklist",
"Dates": "Dates",
"Attachments": "Attachments",
"CustomFields": "Custom Fields",
"Automation": "Automation",
"AddButton": "Add Button",
"Actions": "Actions",
"Move": "Move",
"Copy": "Copy",
"MakeTemplate": "Make Template",
"Watch": "Watch",
"Archive": "Archive",
"HideDetails": "Hide Details",
"ShowDetails": "Show Details"
}
}

View File

@ -1,11 +1,11 @@
{
"string": {
"CreateBoard": "Create board",
"CreateCard": "Create card",
"CardName": "Card name",
"CreateBoard": "Создать",
"CreateCard": "Создать",
"CardName": "Название",
"Cards": "Cards",
"SelectBoard": "Select board",
"More": "More...",
"SelectBoard": "Выбрать",
"More": "Еще...",
"Title": "Title",
"ManageBoardStatuses": "Manage board statuses",
"BoardName": "Board",
@ -19,10 +19,28 @@
"BoardApplication": "Boards",
"Card": "Card",
"Assignee": "Assignee",
"Description": "Description",
"Description": "Описание",
"DescriptionPlaceholder": "Добавьте более подробное описание...",
"Location": "Location",
"Members": "Members",
"Members": "Участники",
"BoardCreateLabel": "Board",
"Settings": "Settings"
"Settings": "Настройки",
"InList": "в списке",
"AddToCard": "Добавить",
"Labels": "Метки",
"Checklist": "Списки",
"Dates": "Дата",
"Attachments": "Прикрепленное",
"CustomFields": "Дополнительно",
"Automation": "Автоматизация",
"AddButton": "Добавить",
"Actions": "Действия",
"Move": "Переместить",
"Copy": "Копировать",
"MakeTemplate": "Шаблон",
"Watch": "Отслеживать",
"Archive": "Архивировать",
"HideDetails": "Спрятать",
"ShowDetails": "Показать"
}
}

View File

@ -12,39 +12,42 @@
"format": "prettier --write --plugin-search-dir=. src && eslint --fix src"
},
"devDependencies": {
"svelte-loader": "^3.1.2",
"sass": "^1.37.5",
"svelte-preprocess": "^4.10.5",
"@anticrm/platform-rig": "~0.6.0",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"eslint": "^7.32.0",
"eslint-config-standard-with-typescript": "^21.0.1",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.1",
"eslint-plugin-svelte3": "~3.2.1",
"prettier-plugin-svelte": "^2.2.0",
"eslint": "^7.32.0",
"prettier": "^2.4.1",
"prettier-plugin-svelte": "^2.2.0",
"sass": "^1.37.5",
"svelte-check": "^2.2.10",
"svelte-loader": "^3.1.2",
"svelte-preprocess": "^4.10.5",
"typescript": "^4.3.5"
},
"dependencies": {
"@anticrm/platform": "~0.6.5",
"svelte": "^3.46",
"@anticrm/board": "~0.6.0",
"@anticrm/ui": "~0.6.0",
"@anticrm/presentation": "~0.6.2",
"@anticrm/core": "~0.6.16",
"@anticrm/panel": "~0.6.0",
"@anticrm/contact": "~0.6.5",
"@anticrm/view": "~0.6.0",
"@anticrm/task": "~0.6.0",
"@anticrm/workbench": "~0.6.1",
"@anticrm/view-resources": "~0.6.0",
"@anticrm/activity": "~0.6.0",
"@anticrm/attachment-resources": "~0.6.0",
"@anticrm/contact-resources": "~0.6.0",
"@anticrm/board": "~0.6.0",
"@anticrm/chunter": "~0.6.1",
"@anticrm/chunter-resources": "~0.6.0",
"@anticrm/notification": "~0.6.0"
"@anticrm/contact": "~0.6.5",
"@anticrm/contact-resources": "~0.6.0",
"@anticrm/core": "~0.6.16",
"@anticrm/notification": "~0.6.0",
"@anticrm/panel": "~0.6.0",
"@anticrm/platform": "~0.6.5",
"@anticrm/presentation": "~0.6.2",
"@anticrm/task": "~0.6.0",
"@anticrm/text-editor": "~0.6.0",
"@anticrm/ui": "~0.6.0",
"@anticrm/view": "~0.6.0",
"@anticrm/view-resources": "~0.6.0",
"@anticrm/workbench": "~0.6.1",
"svelte": "^3.46"
}
}

View File

@ -17,13 +17,13 @@
import type { Card } from '@anticrm/board'
import { Icon, showPanel } from '@anticrm/ui'
import view from '@anticrm/view'
import lead from '../plugin'
import board from '../plugin'
export let value: Card
export let inline: boolean = false
async function show () {
showPanel(view.component.EditDoc, value._id, value._class, 'middle')
showPanel(board.component.EditCard, value._id, value._class, 'middle')
}
</script>
@ -35,7 +35,7 @@
on:click={show}
>
<div class="icon">
<Icon icon={lead.icon.Board} size={'small'} />
<Icon icon={board.icon.Card} size={'small'} />
</div>
<span class="label">{value.title}</span>
</a>

View File

@ -33,22 +33,21 @@
const client = getClient()
async function createFunnel (): Promise<void> {
if (templateId !== undefined && await client.findOne(task.class.KanbanTemplate, { _id: templateId }) === undefined) {
async function createBoard (): Promise<void> {
if (
templateId !== undefined &&
(await client.findOne(task.class.KanbanTemplate, { _id: templateId })) === undefined
) {
throw Error(`Failed to find target kanban template: ${templateId}`)
}
const id = await client.createDoc(
board.class.Board,
core.space.Space,
{
name,
description,
private: false,
archived: false,
members: []
}
)
const id = await client.createDoc(board.class.Board, core.space.Space, {
name,
description,
private: false,
archived: false,
members: []
})
await createKanban(client, id, templateId)
}
@ -56,22 +55,32 @@
<SpaceCreateCard
label={board.string.CreateBoard}
okAction={createFunnel}
okAction={createBoard}
canSave={name.length > 0}
on:close={() => {
dispatch('close')
}}
>
<Grid column={1} rowGap={1.5}>
<EditBox label={board.string.BoardName} icon={IconFolder} bind:value={name} placeholder={board.string.Board} maxWidth={'16rem'} focus />
<EditBox
label={board.string.BoardName}
icon={IconFolder}
bind:value={name}
placeholder={board.string.Board}
maxWidth={'16rem'}
focus
/>
<!-- <ToggleWithLabel label={board.string.MakePrivate} description={board.string.MakePrivateDescription} /> -->
<Component is={task.component.KanbanTemplateSelector} props={{
folders: [board.space.BoardTemplates],
template: templateId
}} on:change={(evt) => {
templateId = evt.detail
}}/>
<Component
is={task.component.KanbanTemplateSelector}
props={{
folders: [board.space.BoardTemplates],
template: templateId
}}
on:change={(evt) => {
templateId = evt.detail
}}
/>
</Grid>
</SpaceCreateCard>

View File

@ -15,18 +15,42 @@
-->
<script lang="ts">
import type { Card } from '@anticrm/board'
import { getClient } from '@anticrm/presentation'
import { EditBox, Grid } from '@anticrm/ui'
import { Class, Ref } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import type { State } from '@anticrm/task'
import task from '@anticrm/task'
import { StyledTextBox } from '@anticrm/text-editor'
import { Button, EditBox, Icon, IconClose, Label, Scroller } from '@anticrm/ui'
import { createEventDispatcher, onMount } from 'svelte'
import board from '../plugin'
import CardActions from './editor/CardActions.svelte'
import CardActivity from './editor/CardActivity.svelte'
import CardFields from './editor/CardFields.svelte'
export let object: Card
export let _id: Ref<Card>
export let _class: Ref<Class<Card>>
const dispatch = createEventDispatcher()
const client = getClient()
const query = createQuery()
let object: Card | undefined
let state: State | undefined
$: _id &&
_class &&
query.query(_class, { _id }, async (result) => {
object = result[0]
})
$: object &&
query.query(task.class.State, { _id: object.state }, async (result) => {
state = result[0]
})
function change (field: string, value: any) {
client.updateDoc(object._class, object.space, object._id, { [field]: value })
if (object) {
client.update(object, { [field]: value })
}
}
onMount(() => {
@ -35,15 +59,74 @@
</script>
{#if object !== undefined}
<Grid column={1} rowGap={1.5}>
<EditBox
label={board.string.CardName}
bind:value={object.title}
icon={board.icon.Card}
placeholder={board.string.CardPlaceholder}
maxWidth="39rem"
focus
on:change={() => change('title', object.title)}
/>
</Grid>
<Scroller>
<div class="flex-col-stretch h-full w-165 p-6">
<!-- TODO cover -->
<div class="close-button">
<Button icon={IconClose} kind="transparent" size="large" on:click={() => dispatch('close')} />
</div>
<div class="flex-row-streach">
<div class="w-9">
<Icon icon={board.icon.Card} size="large" />
</div>
<div class="fs-title text-lg">
<EditBox bind:value={object.title} maxWidth="39rem" focus on:change={() => change('title', object?.title)} />
</div>
</div>
<div class="flex-row-streach">
<div class="w-9" />
<div>
<Label label={board.string.InList} /><span class="state-name ml-1">{state?.title}</span>
</div>
</div>
<div class="flex-row-streach">
<div class="flex-grow mr-4">
<div class="flex-row-streach">
<div class="w-9" />
<CardFields value={object} />
</div>
<div class="flex-row-streach mt-4 mb-2">
<div class="w-9">
<Icon icon={board.icon.Card} size="large" />
</div>
<div class="fs-title">
<Label label={board.string.Description} />
</div>
</div>
<div class="flex-row-streach">
<div class="w-9" />
<div class="background-bg-accent border-bg-accent border-radius-3 p-2 w-full">
<StyledTextBox
alwaysEdit={true}
showButtons={false}
placeholder={board.string.DescriptionPlaceholder}
bind:content={object.description}
on:value={(evt) => change('description', evt.detail)}
/>
</div>
</div>
<!-- TODO attachments-->
<!-- TODO checklists -->
<CardActivity value={object} />
</div>
<CardActions value={object} />
</div>
</div>
</Scroller>
{/if}
<style lang="scss">
.close-button {
position: absolute;
top: 0.7rem;
right: 0.7rem;
}
.state-name {
text-decoration: underline;
&:hover {
color: var(--caption-color);
}
}
</style>

View File

@ -20,7 +20,6 @@
import type { WithLookup } from '@anticrm/core'
import notification from '@anticrm/notification'
import { ActionIcon, Component, IconMoreH, showPanel, showPopup } from '@anticrm/ui'
import view from '@anticrm/view'
import { ContextMenu } from '@anticrm/view-resources'
import board from '../plugin'
@ -33,11 +32,11 @@
}
function showLead () {
showPanel(view.component.EditDoc, object._id, object._class, 'middle')
showPanel(board.component.EditCard, object._id, object._class, 'middle')
}
</script>
<div class="card-container" {draggable} class:draggable on:dragstart on:dragend class:dragged={dragged}>
<div class="card-container" {draggable} class:draggable on:dragstart on:dragend class:dragged>
<div class="flex-between mb-4">
<div class="flex-col">
<div class="fs-title cursor-pointer" on:click={showLead}>{object.title}</div>
@ -56,7 +55,7 @@
/>
</div>
</div>
<div class="flex-between">
<div class="flex-between">
<div class="flex-row-center">
{#if (object.attachments ?? 0) > 0}
<div class="step-lr75"><AttachmentsPresenter value={object} /></div>
@ -72,14 +71,14 @@
.card-container {
display: flex;
flex-direction: column;
padding: .5rem 1rem;
padding: 0.5rem 1rem;
background-color: var(--board-card-bg-color);
border: 1px solid var(--board-card-bg-color);
border-radius: .25rem;
border-radius: 0.25rem;
user-select: none;
&:hover {
background-color: var(--board-card-bg-hover);
&:hover {
background-color: var(--board-card-bg-hover);
}
&.draggable {
cursor: grab;

View File

@ -12,6 +12,6 @@
width: 100%;
height: 100%;
color: #fff;
background-color: #4474F6;
background-color: #4474f6;
}
</style>
</style>

View File

@ -0,0 +1,43 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Card } from '@anticrm/board'
import { Button, Label } from '@anticrm/ui'
import { getEditorCardActionGroups } from '../../utils/CardActionUtils'
export let value: Card
const actionGroups = getEditorCardActionGroups(value)
</script>
{#if value}
<div class="flex-col flex-gap-3">
{#each actionGroups as group}
<div class="flex-col flex-gap-1">
<Label label={group.label} />
{#each group.actions as action}
<Button
icon={action.icon}
label={action.label}
kind={action.isTransparent ? 'transparent' : 'no-border'}
justify="left"
on:click={() => action.handler?.(value)}
/>
{/each}
</div>
{/each}
</div>
{/if}

View File

@ -0,0 +1,59 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import activity from '@anticrm/activity'
import type { Card } from '@anticrm/board'
import chunter from '@anticrm/chunter'
import { Button, Component, Icon, IconActivity, Label } from '@anticrm/ui'
import board from '../../plugin'
export let value: Card | undefined
let isActivityShown: boolean = true
</script>
{#if value !== undefined}
<div class="flex-col-stretch h-full w-full">
<!-- TODO attachments-->
<!-- TODO checklists -->
<div class="flex-row-streach mt-4 mb-2">
<div class="w-9">
<Icon icon={IconActivity} size="large" />
</div>
<div class="flex-grow fs-title">
<Label label={activity.string.Activity} />
</div>
<Button
kind="no-border"
label={isActivityShown ? board.string.HideDetails : board.string.ShowDetails}
width="100px"
on:click={() => {
isActivityShown = !isActivityShown
}}
/>
</div>
<div class="flex-row-streach">
<div class="w-9" />
<div class="w-full">
<Component is={chunter.component.CommentInput} props={{ object: value }} />
</div>
</div>
{#if isActivityShown === true}
<Component is={activity.component.Activity} props={{ object: value, showCommenInput: false, transparent: true }}>
<slot />
</Component>
{/if}
</div>
{/if}

View File

@ -0,0 +1,24 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Card } from '@anticrm/board'
export let value: Card
</script>
{#if value}
<div />
{/if}

View File

@ -0,0 +1,17 @@
import { Card } from '@anticrm/board'
import { Asset, IntlString } from '@anticrm/platform'
import { AnySvelteComponent } from '@anticrm/ui'
export interface CardActionGroup {
actions: CardAction[]
hint?: IntlString
label: IntlString
}
export interface CardAction {
hint?: IntlString
icon: Asset | AnySvelteComponent
isTransparent?: boolean
label: IntlString
handler?: (card: Card) => void
}

View File

@ -41,17 +41,35 @@ export default mergeIds(boardId, board, {
Assignee: '' as IntlString,
ManageBoardStatuses: '' as IntlString,
Description: '' as IntlString,
DescriptionPlaceholder: '' as IntlString,
Location: '' as IntlString,
Members: '' as IntlString,
BoardCreateLabel: '' as IntlString,
Settings: '' as IntlString
Settings: '' as IntlString,
InList: '' as IntlString,
AddToCard: '' as IntlString,
Labels: '' as IntlString,
Checklist: '' as IntlString,
Dates: '' as IntlString,
Attachments: '' as IntlString,
CustomFields: '' as IntlString,
Automation: '' as IntlString,
AddButton: '' as IntlString,
Actions: '' as IntlString,
Move: '' as IntlString,
Copy: '' as IntlString,
MakeTemplate: '' as IntlString,
Watch: '' as IntlString,
Archive: '' as IntlString,
HideDetails: '' as IntlString,
ShowDetails: '' as IntlString
},
component: {
CreateCustomer: '' as AnyComponent,
CardsPresenter: '' as AnyComponent,
Boards: '' as AnyComponent,
EditCard: '' as AnyComponent,
Members: '' as AnyComponent,
Settings: '' as AnyComponent
}
})

View File

@ -0,0 +1,86 @@
import { Card } from '@anticrm/board'
import { IconAdd, IconAttachment } from '@anticrm/ui'
import { CardAction, CardActionGroup } from '../models/CardAction'
import board from '../plugin'
export const MembersAction: CardAction = {
icon: board.icon.Card,
label: board.string.Members
}
export const LabelsAction: CardAction = {
icon: board.icon.Card,
label: board.string.Labels
}
export const ChecklistAction: CardAction = {
icon: board.icon.Card,
label: board.string.Checklist
}
export const DatesAction: CardAction = {
icon: board.icon.Card,
label: board.string.Dates
}
export const AttachmentsAction: CardAction = {
icon: IconAttachment,
label: board.string.Attachments
}
export const CustomFieldsAction: CardAction = {
icon: board.icon.Card,
label: board.string.CustomFields
}
export const AddButtonAction: CardAction = {
icon: IconAdd,
isTransparent: true,
label: board.string.AddButton
}
export const MoveAction: CardAction = {
icon: board.icon.Card,
label: board.string.Move
}
export const CopyAction: CardAction = {
icon: board.icon.Card,
label: board.string.Copy
}
export const MakeTemplateAction: CardAction = {
icon: board.icon.Card,
label: board.string.MakeTemplate
}
export const WatchAction: CardAction = {
icon: board.icon.Card,
label: board.string.Watch
}
export const ArchiveAction: CardAction = {
icon: board.icon.Card,
label: board.string.Archive
}
export const getEditorCardActionGroups = (card: Card): CardActionGroup[] => {
if (card === undefined) {
return []
}
return [
{
label: board.string.AddToCard,
actions: [MembersAction, LabelsAction, ChecklistAction, DatesAction, AttachmentsAction, CustomFieldsAction]
},
{
label: board.string.Automation,
actions: [AddButtonAction]
},
{
label: board.string.Actions,
actions: [MoveAction, CopyAction, MakeTemplateAction, WatchAction, ArchiveAction]
}
]
}