mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-09 01:10:17 +00:00
Use logo for workspaces (#4828)
Signed-off-by: Vyacheslav Tumanov <me@slavatumanov.me>
This commit is contained in:
parent
6cbb497a01
commit
36bc394e7a
@ -28,6 +28,7 @@ import {
|
||||
type Integration,
|
||||
type IntegrationType,
|
||||
type InviteSettings,
|
||||
type WorkspaceSetting,
|
||||
type SettingsCategory,
|
||||
type UserMixin
|
||||
} from '@hcengineering/setting'
|
||||
@ -99,6 +100,11 @@ export class TInviteSettings extends TConfiguration implements InviteSettings {
|
||||
limit!: number
|
||||
}
|
||||
|
||||
@Model(setting.class.WorkspaceSetting, core.class.Doc, DOMAIN_SETTING)
|
||||
export class TWorkspaceSetting extends TDoc implements WorkspaceSetting {
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(
|
||||
TIntegration,
|
||||
@ -107,7 +113,8 @@ export function createModel (builder: Builder): void {
|
||||
TWorkspaceSettingCategory,
|
||||
TEditable,
|
||||
TUserMixin,
|
||||
TInviteSettings
|
||||
TInviteSettings,
|
||||
TWorkspaceSetting
|
||||
)
|
||||
|
||||
builder.mixin(setting.class.Integration, core.class.Class, notification.mixin.ClassCollaborators, {
|
||||
@ -205,6 +212,19 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
setting.ids.Configure
|
||||
)
|
||||
builder.createDoc(
|
||||
setting.class.WorkspaceSettingCategory,
|
||||
core.space.Model,
|
||||
{
|
||||
name: 'workspaceSettings',
|
||||
label: setting.string.Branding,
|
||||
icon: setting.icon.AccountSettings,
|
||||
component: setting.component.WorkspaceSetting,
|
||||
order: 1002,
|
||||
secured: true
|
||||
},
|
||||
setting.ids.WorkspaceSetting
|
||||
)
|
||||
builder.createDoc(
|
||||
setting.class.WorkspaceSettingCategory,
|
||||
core.space.Model,
|
||||
|
@ -20,6 +20,7 @@
|
||||
import presentation from '@hcengineering/presentation'
|
||||
|
||||
export let file: Blob
|
||||
export let lessCrop: boolean = false
|
||||
let inputRef: HTMLInputElement
|
||||
const targetMimes = ['image/png', 'image/jpg', 'image/jpeg']
|
||||
|
||||
@ -64,7 +65,7 @@
|
||||
<div class="editavatar-container">
|
||||
{#await CropperP then Cropper}
|
||||
<div class="cropper">
|
||||
<Cropper bind:this={cropper} image={file} />
|
||||
<Cropper bind:this={cropper} image={file} {lessCrop} />
|
||||
</div>
|
||||
<div class="footer">
|
||||
<Button label={presentation.string.Save} kind={'primary'} size={'large'} on:click={onCrop} />
|
||||
|
@ -35,6 +35,8 @@
|
||||
export let direct: Blob | undefined = undefined
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
export let disabled: boolean = false
|
||||
export let imageOnly: boolean = false
|
||||
export let lessCrop: boolean = false
|
||||
|
||||
$: [schema, uri] = avatar?.split('://') || []
|
||||
|
||||
@ -91,6 +93,8 @@
|
||||
name,
|
||||
file: direct,
|
||||
icon,
|
||||
imageOnly,
|
||||
lessCrop,
|
||||
onSubmit: handlePopupSubmit
|
||||
})
|
||||
}
|
||||
|
@ -41,6 +41,8 @@
|
||||
export let email: string | undefined
|
||||
export let file: Blob | undefined
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
export let imageOnly: boolean = false
|
||||
export let lessCrop: boolean = false
|
||||
export let onSubmit: (avatarType?: AvatarType, avatar?: string, file?: Blob) => void
|
||||
|
||||
const [schema, uri] = avatar?.split('://') || []
|
||||
@ -110,18 +112,18 @@
|
||||
|
||||
if (selectedFile !== undefined) {
|
||||
editableFile = selectedFile
|
||||
} else if (selectedAvatar) {
|
||||
} else if (selectedAvatar && !(imageOnly && selectedAvatar === initialSelectedAvatar)) {
|
||||
const url = getFileUrl(selectedAvatar, 'full')
|
||||
editableFile = await (await fetch(url)).blob()
|
||||
} else {
|
||||
inputRef.click()
|
||||
return
|
||||
}
|
||||
showCropper(editableFile)
|
||||
if (editableFile.size > 0) showCropper(editableFile)
|
||||
}
|
||||
|
||||
function showCropper (editableFile: Blob) {
|
||||
showPopup(EditAvatarPopup, { file: editableFile }, undefined, (blob) => {
|
||||
showPopup(EditAvatarPopup, { file: editableFile, lessCrop }, undefined, (blob) => {
|
||||
if (blob === undefined) {
|
||||
if (!selectedFile && (!avatar || avatar.includes('://'))) {
|
||||
selectedAvatarType = AvatarType.COLOR
|
||||
@ -131,7 +133,7 @@
|
||||
}
|
||||
if (blob === null) {
|
||||
selectedAvatarType = AvatarType.COLOR
|
||||
selectedAvatar = getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name
|
||||
selectedAvatar = imageOnly ? '' : getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name
|
||||
selectedFile = undefined
|
||||
} else {
|
||||
selectedFile = blob
|
||||
@ -203,8 +205,12 @@
|
||||
<div
|
||||
class="cursor-pointer"
|
||||
on:click|self={(e) => {
|
||||
if (selectedAvatarType === AvatarType.IMAGE) handleImageAvatarClick()
|
||||
else if (selectedAvatarType === AvatarType.COLOR) showColorPopup(e)
|
||||
if (imageOnly) {
|
||||
handleImageAvatarClick()
|
||||
} else {
|
||||
if (selectedAvatarType === AvatarType.IMAGE) handleImageAvatarClick()
|
||||
else if (selectedAvatarType === AvatarType.COLOR) showColorPopup(e)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AvatarComponent
|
||||
@ -220,7 +226,7 @@
|
||||
/>
|
||||
</div>
|
||||
<TabList
|
||||
items={getAvatarTypeDropdownItems(hasGravatar)}
|
||||
items={getAvatarTypeDropdownItems(hasGravatar, imageOnly)}
|
||||
kind={'separated-free'}
|
||||
bind:selected={selectedAvatarType}
|
||||
on:select={handleDropdownSelection}
|
||||
|
@ -335,7 +335,15 @@ function fillStores (): void {
|
||||
|
||||
fillStores()
|
||||
|
||||
export function getAvatarTypeDropdownItems (hasGravatar: boolean): TabItem[] {
|
||||
export function getAvatarTypeDropdownItems (hasGravatar: boolean, imageOnly?: boolean): TabItem[] {
|
||||
if (imageOnly === true) {
|
||||
return [
|
||||
{
|
||||
id: AvatarType.IMAGE,
|
||||
labelIntl: contact.string.UseImage
|
||||
}
|
||||
]
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: AvatarType.COLOR,
|
||||
|
@ -18,6 +18,7 @@
|
||||
|
||||
export let image: Blob
|
||||
export let cropSize = 1200
|
||||
export let lessCrop: boolean = false
|
||||
|
||||
let imgRef: HTMLImageElement
|
||||
let cropper: Cropper | undefined
|
||||
@ -86,7 +87,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full flex">
|
||||
<div class="w-full h-full flex" class:less-crop={lessCrop}>
|
||||
<img class="image" bind:this={imgRef} alt="img" />
|
||||
{#await init(image)}
|
||||
Waiting...
|
||||
@ -99,6 +100,9 @@
|
||||
:global(.cropper-view-box, .cropper-face) {
|
||||
border-radius: 50%;
|
||||
}
|
||||
:global(.less-crop .cropper-view-box, .less-crop .cropper-face) {
|
||||
border-radius: 10%;
|
||||
}
|
||||
|
||||
.image {
|
||||
max-width: 100%;
|
||||
|
@ -99,6 +99,7 @@
|
||||
"TaskTypes": "Task types",
|
||||
"Automations": "Automations",
|
||||
"Collections": "Collections",
|
||||
"ClassColon": "Class:"
|
||||
"ClassColon": "Class:",
|
||||
"Branding": "Branding"
|
||||
}
|
||||
}
|
@ -100,6 +100,7 @@
|
||||
"TaskTypes": "Типы задач",
|
||||
"Automations": "Автоматизация",
|
||||
"Collections": "Коллекции",
|
||||
"ClassColon": "Класс:"
|
||||
"ClassColon": "Класс:",
|
||||
"Branding": "Брендинг"
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import contact, { Employee, PersonAccount, combineName, getFirstName, getLastName } from '@hcengineering/contact'
|
||||
import { ChannelsEditor, EditableAvatar, employeeByIdStore } from '@hcengineering/contact-resources'
|
||||
import { AttributeEditor, getClient, MessageBox } from '@hcengineering/presentation'
|
||||
import {
|
||||
Button,
|
||||
createFocusManager,
|
||||
EditBox,
|
||||
FocusHandler,
|
||||
showPopup,
|
||||
Header,
|
||||
Breadcrumb,
|
||||
Label
|
||||
} from '@hcengineering/ui'
|
||||
import setting from '../plugin'
|
||||
import { WorkspaceSetting } from '@hcengineering/setting'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
|
||||
export let visibleNav: boolean = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let workspaceSettings: WorkspaceSetting | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
client.findOne(setting.class.WorkspaceSetting, {}).then((r) => {
|
||||
workspaceSettings = r
|
||||
})
|
||||
|
||||
let avatarEditor: EditableAvatar
|
||||
|
||||
async function onAvatarDone (e: any): Promise<void> {
|
||||
if (workspaceSettings === undefined) {
|
||||
const avatar = await avatarEditor.createAvatar()
|
||||
await client.createDoc(
|
||||
setting.class.WorkspaceSetting,
|
||||
setting.space.Setting,
|
||||
{ icon: avatar },
|
||||
setting.ids.WorkspaceSetting
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (workspaceSettings.icon != null) {
|
||||
await avatarEditor.removeAvatar(workspaceSettings.icon)
|
||||
}
|
||||
const avatar = await avatarEditor.createAvatar()
|
||||
await client.update(workspaceSettings, {
|
||||
icon: avatar
|
||||
})
|
||||
}
|
||||
|
||||
const manager = createFocusManager()
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
|
||||
<div class="hulyComponent p-10 flex ac-body row">
|
||||
<EditableAvatar
|
||||
avatar={workspaceSettings?.icon}
|
||||
size={'x-large'}
|
||||
bind:this={avatarEditor}
|
||||
on:done={onAvatarDone}
|
||||
imageOnly
|
||||
lessCrop
|
||||
/>
|
||||
<div class="heading-medium-20 p-4">
|
||||
<Label label={getEmbeddedLabel('Workspace Logo')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.row {
|
||||
flex-direction: row;
|
||||
align-content: center;
|
||||
}
|
||||
</style>
|
@ -40,6 +40,7 @@ import NumberTypeEditor from './components/typeEditors/NumberTypeEditor.svelte'
|
||||
import ArrayEditor from './components/typeEditors/ArrayEditor.svelte'
|
||||
import RefEditor from './components/typeEditors/RefEditor.svelte'
|
||||
import StringTypeEditor from './components/typeEditors/StringTypeEditor.svelte'
|
||||
import WorkspaceSetting from './components/WorkspaceSetting.svelte'
|
||||
import WorkspaceSettings from './components/WorkspaceSettings.svelte'
|
||||
import InviteSetting from './components/InviteSetting.svelte'
|
||||
import Configure from './components/Configure.svelte'
|
||||
@ -84,6 +85,7 @@ export default async (): Promise<Resources> => ({
|
||||
Settings,
|
||||
Profile,
|
||||
Password,
|
||||
WorkspaceSetting,
|
||||
WorkspaceSettings,
|
||||
Integrations,
|
||||
Support,
|
||||
|
@ -97,6 +97,13 @@ export interface InviteSettings extends Configuration {
|
||||
limit: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface WorkspaceSetting extends Doc {
|
||||
icon?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -114,7 +121,8 @@ export default plugin(settingId, {
|
||||
Terms: '' as Ref<Doc>,
|
||||
ClassSetting: '' as Ref<Doc>,
|
||||
Owners: '' as Ref<Doc>,
|
||||
InviteSettings: '' as Ref<Doc>
|
||||
InviteSettings: '' as Ref<Doc>,
|
||||
WorkspaceSetting: '' as Ref<Doc>
|
||||
},
|
||||
mixin: {
|
||||
Editable: '' as Ref<Mixin<Editable>>,
|
||||
@ -128,12 +136,14 @@ export default plugin(settingId, {
|
||||
WorkspaceSettingCategory: '' as Ref<Class<SettingsCategory>>,
|
||||
Integration: '' as Ref<Class<Integration>>,
|
||||
IntegrationType: '' as Ref<Class<IntegrationType>>,
|
||||
InviteSettings: '' as Ref<Class<InviteSettings>>
|
||||
InviteSettings: '' as Ref<Class<InviteSettings>>,
|
||||
WorkspaceSetting: '' as Ref<Class<WorkspaceSetting>>
|
||||
},
|
||||
component: {
|
||||
Settings: '' as AnyComponent,
|
||||
Profile: '' as AnyComponent,
|
||||
Password: '' as AnyComponent,
|
||||
WorkspaceSetting: '' as AnyComponent,
|
||||
WorkspaceSettings: '' as AnyComponent,
|
||||
Integrations: '' as AnyComponent,
|
||||
Support: '' as AnyComponent,
|
||||
@ -145,6 +155,7 @@ export default plugin(settingId, {
|
||||
Settings: '' as IntlString,
|
||||
Setting: '' as IntlString,
|
||||
WorkspaceSettings: '' as IntlString,
|
||||
Branding: '' as IntlString,
|
||||
Integrations: '' as IntlString,
|
||||
Support: '' as IntlString,
|
||||
Privacy: '' as IntlString,
|
||||
|
@ -13,11 +13,32 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import setting, { WorkspaceSetting } from '@hcengineering/setting'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Component, Icon } from '@hcengineering/ui'
|
||||
import contact, { type GetAvatarUrl } from '@hcengineering/contact'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
|
||||
export let mini: boolean = false
|
||||
export let workspace: string
|
||||
const wsSettingQuery = createQuery()
|
||||
let getFileUrl: undefined | GetAvatarUrl = undefined
|
||||
getResource(contact.function.GetFileUrl).then((r) => (getFileUrl = r))
|
||||
const client = getClient()
|
||||
let workspaceSetting: WorkspaceSetting | undefined = undefined
|
||||
wsSettingQuery.query(setting.class.WorkspaceSetting, {}, (res) => {
|
||||
workspaceSetting = res[0]
|
||||
})
|
||||
$: url =
|
||||
getFileUrl !== undefined && workspaceSetting?.icon != null ? getFileUrl(workspaceSetting.icon, 'large') : ['']
|
||||
$: srcset = url?.slice(1)?.join(', ')
|
||||
</script>
|
||||
|
||||
<div class="antiLogo red" class:mini>{workspace?.toUpperCase()?.[0] ?? ''}</div>
|
||||
{#if getFileUrl !== undefined && workspaceSetting?.icon != null}
|
||||
<img class="logo-medium" src={url[0]} {srcset} alt={''} />
|
||||
{:else}
|
||||
<div class="antiLogo red" class:mini>{workspace?.toUpperCase()?.[0] ?? ''}</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.antiLogo {
|
||||
@ -46,4 +67,11 @@
|
||||
background-color: rgb(246, 105, 77);
|
||||
}
|
||||
}
|
||||
.logo-medium {
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
Reference in New Issue
Block a user