Skip to content

Commit 41814bf

Browse files
committed
refactor: add Status and DocumentStatus components
`Editor/Status` handles the status display in the menubar. `Editor/DocumentStatus` handles the document messages on conflict etc. Also move all files that are only used within it into the `Components/Editor` dir. Signed-off-by: Max <[email protected]>
1 parent 5db2963 commit 41814bf

File tree

6 files changed

+295
-124
lines changed

6 files changed

+295
-124
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<!--
2+
- @copyright Copyright (c) 2022 Max <[email protected]>
3+
-
4+
- @author Max <[email protected]>
5+
-
6+
- @license GNU AGPL version 3 or any later version
7+
-
8+
- This program is free software: you can redistribute it and/or modify
9+
- it under the terms of the GNU Affero General Public License as
10+
- published by the Free Software Foundation, either version 3 of the
11+
- License, or (at your option) any later version.
12+
-
13+
- This program is distributed in the hope that it will be useful,
14+
- but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
- GNU Affero General Public License for more details.
17+
-
18+
- You should have received a copy of the GNU Affero General Public License
19+
- along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
-
21+
-->
22+
23+
<template>
24+
<div class="document-status">
25+
<p v-if="idle" class="msg">
26+
{{ t('text', 'Document idle for {timeout} minutes, click to continue editing', { timeout: IDLE_TIMEOUT }) }} <a class="button primary" @click="reconnect">{{ t('text', 'Reconnect') }}</a>
27+
</p>
28+
<p v-else-if="hasSyncCollission" class="msg icon-error">
29+
{{ t('text', 'The document has been changed outside of the editor. The changes cannot be applied.') }}
30+
</p>
31+
<p v-else-if="hasConnectionIssue" class="msg">
32+
{{ t('text', 'File could not be loaded. Please check your internet connection.') }} <a class="button primary" @click="reconnect">{{ t('text', 'Reconnect') }}</a>
33+
</p>
34+
<p v-if="lock" class="msg msg-locked">
35+
<Lock /> {{ t('text', 'This file is opened read-only as it is currently locked by {user}.', { user: lock.displayName }) }}
36+
</p>
37+
</div>
38+
</template>
39+
40+
<script>
41+
42+
import { ERROR_TYPE, IDLE_TIMEOUT } from './../../services/SyncService.js'
43+
import Lock from 'vue-material-design-icons/Lock'
44+
45+
export default {
46+
name: 'DocumentStatus',
47+
48+
components: {
49+
Lock,
50+
},
51+
52+
props: {
53+
idle: {
54+
type: Boolean,
55+
require: true,
56+
},
57+
lock: {
58+
type: Object,
59+
default: null,
60+
},
61+
syncError: {
62+
type: Object,
63+
default: null,
64+
},
65+
hasConnectionIssue: {
66+
type: Boolean,
67+
require: true,
68+
},
69+
},
70+
71+
computed: {
72+
hasSyncCollission() {
73+
return this.syncError && this.syncError.type === ERROR_TYPE.SAVE_COLLISSION
74+
},
75+
},
76+
77+
methods: {
78+
reconnect() {
79+
this.$emit('reconnect')
80+
},
81+
},
82+
83+
}
84+
</script>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
3939
import { generateUrl } from '@nextcloud/router'
4040
import AvatarWrapper from './AvatarWrapper.vue'
41-
import { useSyncServiceMixin } from './EditorWrapper.provider.js'
41+
import { useSyncServiceMixin } from '../EditorWrapper.provider.js'
4242
4343
export default {
4444
name: 'GuestNameDialog',
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
import Popover from '@nextcloud/vue/dist/Components/Popover'
6464
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
6565
import AvatarWrapper from './AvatarWrapper.vue'
66-
import store from '../mixins/store.js'
66+
import store from '../../mixins/store.js'
6767
6868
const COLLABORATOR_IDLE_TIME = 60
6969
const COLLABORATOR_DISCONNECT_TIME = 90

src/components/Editor/Status.vue

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
<!--
2+
- @copyright Copyright (c) 2022 Max <[email protected]>
3+
-
4+
- @author Max <[email protected]>
5+
-
6+
- @license GNU AGPL version 3 or any later version
7+
-
8+
- This program is free software: you can redistribute it and/or modify
9+
- it under the terms of the GNU Affero General Public License as
10+
- published by the Free Software Foundation, either version 3 of the
11+
- License, or (at your option) any later version.
12+
-
13+
- This program is distributed in the hope that it will be useful,
14+
- but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
- GNU Affero General Public License for more details.
17+
-
18+
- You should have received a copy of the GNU Affero General Public License
19+
- along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
-
21+
-->
22+
23+
<template>
24+
<div class="text-editor__session-list">
25+
<div v-if="$isMobile" v-tooltip="lastSavedStatusTooltip" :class="saveStatusClass" />
26+
<div v-else
27+
v-tooltip="lastSavedStatusTooltip"
28+
class="save-status"
29+
:class="lastSavedStatusClass">
30+
{{ lastSavedStatus }}
31+
</div>
32+
<SessionList :sessions="sessions">
33+
<p slot="lastSaved" class="last-saved">
34+
{{ t('text', 'Last saved') }}: {{ lastSavedString }}
35+
</p>
36+
<GuestNameDialog v-if="$isPublic && !currentSession.userId" :session="currentSession" />
37+
</SessionList>
38+
</div>
39+
</template>
40+
41+
<script>
42+
43+
import { ERROR_TYPE } from './../../services/SyncService.js'
44+
import {
45+
useIsMobileMixin,
46+
useIsPublicMixin,
47+
} from '../EditorWrapper.provider.js'
48+
49+
export default {
50+
name: 'Status',
51+
52+
components: {
53+
SessionList: () => import(/* webpackChunkName: "editor-collab" */'./SessionList.vue'),
54+
GuestNameDialog: () => import(/* webpackChunkName: "editor-guest" */'./GuestNameDialog.vue'),
55+
},
56+
57+
mixins: [useIsMobileMixin, useIsPublicMixin],
58+
59+
props: {
60+
hasConnectionIssue: {
61+
type: Boolean,
62+
require: true,
63+
},
64+
dirty: {
65+
type: Boolean,
66+
require: true,
67+
},
68+
lastSavedString: {
69+
type: String,
70+
default: '',
71+
},
72+
document: {
73+
type: Object,
74+
default: null,
75+
},
76+
syncError: {
77+
type: Object,
78+
default: null,
79+
},
80+
sessions: {
81+
type: Object,
82+
default: () => { return {} },
83+
},
84+
},
85+
86+
computed: {
87+
lastSavedStatus() {
88+
if (this.hasConnectionIssue) {
89+
return t('text',
90+
this.$isMobile
91+
? 'Offline'
92+
: 'Offline, changes will be saved when online'
93+
)
94+
}
95+
return this.dirtyStateIndicator ? t('text', 'Saving …') : t('text', 'Saved')
96+
},
97+
lastSavedStatusClass() {
98+
return this.syncError && this.lastSavedString !== '' ? 'error' : ''
99+
},
100+
dirtyStateIndicator() {
101+
return this.dirty || this.hasUnsavedChanges
102+
},
103+
lastSavedStatusTooltip() {
104+
let message = t('text', 'Last saved {lastSaved}', { lastSaved: this.lastSavedString })
105+
if (this.hasSyncCollission) {
106+
message = t('text', 'The document has been changed outside of the editor. The changes cannot be applied.')
107+
}
108+
if (this.dirty || this.hasUnsavedChanges) {
109+
message += ' - ' + t('text', 'Unsaved changes')
110+
}
111+
return { content: message, placement: 'bottom' }
112+
},
113+
114+
hasUnsavedChanges() {
115+
return this.document && this.document.lastSavedVersion < this.document.currentVersion
116+
},
117+
hasSyncCollission() {
118+
return this.syncError && this.syncError.type === ERROR_TYPE.SAVE_COLLISSION
119+
},
120+
saveStatusClass() {
121+
if (this.syncError && this.lastSavedString !== '') {
122+
return 'save-error'
123+
}
124+
return this.dirtyStateIndicator ? 'saving-status' : 'saved-status'
125+
},
126+
currentSession() {
127+
return Object.values(this.sessions).find((session) => session.isCurrent)
128+
},
129+
},
130+
}
131+
</script>
132+
133+
<style scoped lang="scss">
134+
135+
.text-editor__session-list {
136+
display: flex;
137+
138+
input, div {
139+
vertical-align: middle;
140+
margin-left: 3px;
141+
}
142+
}
143+
144+
.save-status {
145+
display: inline-flex;
146+
padding: 0;
147+
text-overflow: ellipsis;
148+
color: var(--color-text-lighter);
149+
position: relative;
150+
top: 9px;
151+
min-width: 85px;
152+
max-height: 36px;
153+
154+
&.error {
155+
background-color: var(--color-error);
156+
color: var(--color-main-background);
157+
border-radius: 3px;
158+
}
159+
}
160+
</style>
161+
162+
<style lang="scss">
163+
.saved-status,.saving-status {
164+
display: inline-flex;
165+
padding: 0;
166+
text-overflow: ellipsis;
167+
color: var(--color-text-lighter);
168+
position: relative;
169+
background-color: white;
170+
width: 38px !important;
171+
height: 38px !important;
172+
left: 25%;
173+
z-index: 2;
174+
top: 0px;
175+
}
176+
177+
.saved-status {
178+
border: 2px solid #04AA6D;
179+
border-radius: 50%;
180+
}
181+
182+
.saving-status {
183+
border: 2px solid #f3f3f3;
184+
border-top: 2px solid #3498db;
185+
border-radius: 50%;
186+
animation: spin 2s linear infinite;
187+
}
188+
189+
.last-saved {
190+
padding: 6px;
191+
}
192+
</style>

0 commit comments

Comments
 (0)