Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117927095
D1384.1775453821.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
29 KB
Referenced Files
None
Subscribers
None
D1384.1775453821.diff
View Options
diff --git a/src/public/favicon.ico b/src/public/favicon.ico
deleted file mode 100644
diff --git a/src/public/images/favicon.ico b/src/public/images/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
literal 0
Hc$@<O00001
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -162,6 +162,11 @@
return this.price(cost * units) + '/month' + index
},
+ clickRecord(event) {
+ if (!/^(a|button|svg|path)$/i.test(event.target.nodeName)) {
+ $(event.target).closest('tr').find('a')[0].click()
+ }
+ },
domainStatusClass(domain) {
if (domain.isDeleted) {
return 'text-muted'
diff --git a/src/resources/js/bootstrap.js b/src/resources/js/bootstrap.js
--- a/src/resources/js/bootstrap.js
+++ b/src/resources/js/bootstrap.js
@@ -52,7 +52,11 @@
window.router = new VueRouter({
mode: 'history',
- routes: window.routes
+ routes: window.routes,
+ scrollBehavior (to, from, savedPosition) {
+ // Scroll the page to top, but not on Back action
+ return savedPosition || { x: 0, y: 0 }
+ }
})
router.beforeEach((to, from, next) => {
@@ -71,8 +75,10 @@
})
router.afterEach((to, from) => {
- // Remove the (old) error page when changing a page
- $('#error-page').remove()
+ // When changing a page remove old:
+ // - error page
+ // - modal backdrop
+ $('#error-page,.modal-backdrop.show').remove()
})
/**
diff --git a/src/resources/sass/app.scss b/src/resources/sass/app.scss
--- a/src/resources/sass/app.scss
+++ b/src/resources/sass/app.scss
@@ -1,11 +1,5 @@
-// Fonts
-
-// Variables
@import 'variables';
-
-// Bootstrap
-@import '~bootstrap/scss/bootstrap';
-
+@import 'bootstrap';
@import 'menu';
@import 'toast';
@import 'forms';
@@ -149,6 +143,10 @@
padding-right: 0;
}
}
+
+ button {
+ line-height: 1;
+ }
}
.btn-action {
@@ -266,15 +264,66 @@
}
}
-// Bootstrap style fix
-.btn-link {
- border: 0;
-}
+// Various improvements for mobile
+@include media-breakpoint-down(sm) {
+ .card {
+ border: 0;
+ }
-.table thead th {
- border: 0;
-}
+ .card-body {
+ padding: 0.5rem 0;
+ }
+
+ .form-group {
+ margin-bottom: 0.5rem;
+ }
+
+ .tab-content {
+ margin-top: 0.5rem;
+ }
+
+ .col-form-label {
+ color: #666;
+ font-size: 95%;
+ }
+
+ .form-group.plaintext .col-form-label {
+ padding-bottom: 0;
+ }
+
+ form.read-only.short label {
+ width: 35%;
-small {
- font-size: 0.875em;
+ & + * {
+ width: 65%;
+ }
+ }
+
+ #app > div.container {
+ margin-bottom: 1rem;
+ margin-top: 1rem;
+ }
+
+ #header-menu-navbar {
+ padding: 0;
+ }
+
+ #dashboard-nav > a {
+ width: 135px;
+ }
+
+ .table-sm:not(.form-list) {
+ tbody td {
+ padding: 0.75rem 0.5rem;
+
+ svg {
+ vertical-align: -0.175em;
+ }
+
+ & > svg {
+ font-size: 125%;
+ margin-right: 0.25rem;
+ }
+ }
+ }
}
diff --git a/src/resources/sass/bootstrap.scss b/src/resources/sass/bootstrap.scss
new file mode 100644
--- /dev/null
+++ b/src/resources/sass/bootstrap.scss
@@ -0,0 +1,15 @@
+// Bootstrap
+@import '~bootstrap/scss/bootstrap';
+
+// Bootstrap style fixes
+.btn-link {
+ border: 0;
+}
+
+.table thead th {
+ border: 0;
+}
+
+small {
+ font-size: 0.875em;
+}
diff --git a/src/resources/sass/forms.scss b/src/resources/sass/forms.scss
--- a/src/resources/sass/forms.scss
+++ b/src/resources/sass/forms.scss
@@ -1,4 +1,3 @@
-
.list-input {
& > div {
&:not(:last-child) {
@@ -23,6 +22,10 @@
input.is-invalid {
z-index: 2;
}
+
+ .btn svg {
+ vertical-align: middle;
+ }
}
.range-input {
diff --git a/src/resources/sass/menu.scss b/src/resources/sass/menu.scss
--- a/src/resources/sass/menu.scss
+++ b/src/resources/sass/menu.scss
@@ -116,6 +116,16 @@
@include media-breakpoint-down(sm) {
#header-menu {
padding: 0 1em;
+
+ .navbar-nav {
+ display: block;
+ width: 100%;
+ padding: 0;
+
+ li {
+ border-top: 1px solid #eee;
+ }
+ }
}
#footer-menu {
diff --git a/src/resources/sass/toast.scss b/src/resources/sass/toast.scss
--- a/src/resources/sass/toast.scss
+++ b/src/resources/sass/toast.scss
@@ -5,6 +5,11 @@
margin: 0.5rem;
width: 320px;
z-index: 1055; // above Bootstrap's modal backdrop and dialogs
+
+ @media (max-width: 375px) {
+ left: 0;
+ width: auto;
+ }
}
.toast {
@@ -13,6 +18,10 @@
&:not(:last-child) {
margin-bottom: 0.3rem;
}
+
+ @media (max-width: 375px) {
+ max-width: 100%;
+ }
}
.toast-header {
diff --git a/src/resources/views/layouts/app.blade.php b/src/resources/views/layouts/app.blade.php
--- a/src/resources/views/layouts/app.blade.php
+++ b/src/resources/views/layouts/app.blade.php
@@ -9,6 +9,7 @@
<title>{{ config('app.name') }} -- @yield('title')</title>
{{-- TODO: PWA disabled for now: @laravelPWA --}}
+ <link rel="icon" type="image/x-icon" href="{{ asset('images/favicon.ico') }}">
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue
--- a/src/resources/vue/Admin/User.vue
+++ b/src/resources/vue/Admin/User.vue
@@ -4,8 +4,8 @@
<div class="card-body">
<h1 class="card-title">{{ user.email }}</h1>
<div class="card-text">
- <form class="read-only">
- <div v-if="user.wallet.user_id != user.id" class="form-group row">
+ <form class="read-only short">
+ <div v-if="user.wallet.user_id != user.id" class="form-group row plaintext">
<label for="manager" class="col-sm-4 col-form-label">Managed by</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="manager">
@@ -13,7 +13,7 @@
</span>
</div>
</div>
- <div class="form-group row">
+ <div class="form-group row plaintext">
<label for="userid" class="col-sm-4 col-form-label">ID <span class="text-muted">(Created at)</span></label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="userid">
@@ -21,7 +21,7 @@
</span>
</div>
</div>
- <div class="form-group row">
+ <div class="form-group row plaintext">
<label for="status" class="col-sm-4 col-form-label">Status</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="status">
@@ -29,31 +29,31 @@
</span>
</div>
</div>
- <div class="form-group row" v-if="user.first_name">
+ <div class="form-group row plaintext" v-if="user.first_name">
<label for="first_name" class="col-sm-4 col-form-label">First name</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="first_name">{{ user.first_name }}</span>
</div>
</div>
- <div class="form-group row" v-if="user.last_name">
+ <div class="form-group row plaintext" v-if="user.last_name">
<label for="last_name" class="col-sm-4 col-form-label">Last name</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="last_name">{{ user.last_name }}</span>
</div>
</div>
- <div class="form-group row" v-if="user.organization">
+ <div class="form-group row plaintext" v-if="user.organization">
<label for="organization" class="col-sm-4 col-form-label">Organization</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="organization">{{ user.organization }}</span>
</div>
</div>
- <div class="form-group row" v-if="user.phone">
+ <div class="form-group row plaintext" v-if="user.phone">
<label for="phone" class="col-sm-4 col-form-label">Phone</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="phone">{{ user.phone }}</span>
</div>
</div>
- <div class="form-group row">
+ <div class="form-group row plaintext">
<label for="external_email" class="col-sm-4 col-form-label">External email</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="external_email">
@@ -62,13 +62,13 @@
</span>
</div>
</div>
- <div class="form-group row" v-if="user.billing_address">
+ <div class="form-group row plaintext" v-if="user.billing_address">
<label for="billing_address" class="col-sm-4 col-form-label">Address</label>
<div class="col-sm-8">
<span class="form-control-plaintext" style="white-space:pre" id="billing_address">{{ user.billing_address }}</span>
</div>
</div>
- <div class="form-group row">
+ <div class="form-group row plaintext">
<label for="country" class="col-sm-4 col-form-label">Country</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="country">{{ user.country }}</span>
@@ -114,7 +114,7 @@
<div class="card-body">
<h2 class="card-title">Account balance <span :class="wallet.balance < 0 ? 'text-danger' : 'text-success'"><strong>{{ $root.price(wallet.balance) }}</strong></span></h2>
<div class="card-text">
- <form class="read-only">
+ <form class="read-only short">
<div class="form-group row">
<label class="col-sm-4 col-form-label">Discount</label>
<div class="col-sm-8">
@@ -212,7 +212,7 @@
</tr>
</thead>
<tbody>
- <tr v-for="domain in domains" :id="'domain' + domain.id" :key="domain.id">
+ <tr v-for="domain in domains" :id="'domain' + domain.id" :key="domain.id" @click="$root.clickRecord">
<td>
<svg-icon icon="globe" :class="$root.domainStatusClass(domain)" :title="$root.domainStatusText(domain)"></svg-icon>
<router-link :to="{ path: '/domain/' + domain.id }">{{ domain.namespace }}</router-link>
@@ -238,7 +238,7 @@
</tr>
</thead>
<tbody>
- <tr v-for="item in users" :id="'user' + item.id" :key="item.id">
+ <tr v-for="item in users" :id="'user' + item.id" :key="item.id" @click="$root.clickRecord">
<td>
<svg-icon icon="user" :class="$root.userStatusClass(item)" :title="$root.userStatusText(item)"></svg-icon>
<router-link :to="{ path: '/user/' + item.id }">{{ item.email }}</router-link>
@@ -319,7 +319,7 @@
<div class="modal-body">
<form data-validation-prefix="oneoff_">
<div class="form-group">
- <label for="oneoff_amount">Amount</label>
+ <label for="oneoff_amount" class="col-form-label">Amount</label>
<div class="input-group">
<input type="text" class="form-control" id="oneoff_amount" v-model="oneoff_amount" required>
<span class="input-group-append">
@@ -328,7 +328,7 @@
</div>
</div>
<div class="form-group">
- <label for="oneoff_description">Description</label>
+ <label for="oneoff_description" class="col-form-label">Description</label>
<input class="form-control" id="oneoff_description" v-model="oneoff_description" required>
</div>
</form>
diff --git a/src/resources/vue/Domain/List.vue b/src/resources/vue/Domain/List.vue
--- a/src/resources/vue/Domain/List.vue
+++ b/src/resources/vue/Domain/List.vue
@@ -12,7 +12,7 @@
</tr>
</thead>
<tbody>
- <tr v-for="domain in domains" :key="domain.id">
+ <tr v-for="domain in domains" :key="domain.id" @click="$root.clickRecord">
<td>
<svg-icon icon="globe" :class="$root.domainStatusClass(domain)" :title="$root.domainStatusText(domain)"></svg-icon>
<router-link :to="{ path: 'domain/' + domain.id }">{{ domain.namespace }}</router-link>
diff --git a/src/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue
--- a/src/resources/vue/User/Info.vue
+++ b/src/resources/vue/User/Info.vue
@@ -8,7 +8,7 @@
<div class="card-title" v-if="user_id === 'new'">New user account</div>
<div class="card-text">
<form @submit.prevent="submit">
- <div v-if="user_id !== 'new'" class="form-group row">
+ <div v-if="user_id !== 'new'" class="form-group row plaintext">
<label for="first_name" class="col-sm-4 col-form-label">Status</label>
<div class="col-sm-8">
<span :class="$root.userStatusClass(user) + ' form-control-plaintext'" id="status">{{ $root.userStatusText(user) }}</span>
@@ -71,10 +71,14 @@
<tbody>
<tr v-for="pkg in packages" :id="'p' + pkg.id" :key="pkg.id">
<td class="selection">
- <input type="checkbox" :value="pkg.id" @click="selectPackage" :checked="pkg.id == package_id">
+ <input type="checkbox" @click="selectPackage"
+ :value="pkg.id"
+ :checked="pkg.id == package_id"
+ :id="'pkg-input-' + pkg.id"
+ >
</td>
<td class="name">
- {{ pkg.name }}
+ <label :for="'pkg-input-' + pkg.id">{{ pkg.name }}</label>
</td>
<td class="price text-nowrap">
{{ $root.priceLabel(pkg.cost, 1, discount) }}
@@ -113,11 +117,11 @@
:value="sku.id"
:disabled="sku.readonly"
:checked="sku.enabled"
- :dusk="'sku-input-' + sku.title"
+ :id="'sku-input-' + sku.title"
>
</td>
<td class="name">
- <span class="name">{{ sku.name }}</span>
+ <label :for="'sku-input-' + sku.title">{{ sku.name }}</label>
<div v-if="sku.range" class="range-input">
<label class="text-nowrap">{{ sku.range.min }} {{ sku.range.unit }}</label>
<input
diff --git a/src/resources/vue/User/List.vue b/src/resources/vue/User/List.vue
--- a/src/resources/vue/User/List.vue
+++ b/src/resources/vue/User/List.vue
@@ -17,7 +17,7 @@
</tr>
</thead>
<tbody>
- <tr v-for="user in users" :id="'user' + user.id" :key="user.id">
+ <tr v-for="user in users" :id="'user' + user.id" :key="user.id" @click="$root.clickRecord">
<td>
<svg-icon icon="user" :class="$root.userStatusClass(user)" :title="$root.userStatusText(user)"></svg-icon>
<router-link :to="{ path: 'user/' + user.id }">{{ user.email }}</router-link>
diff --git a/src/resources/vue/Widgets/Status.vue b/src/resources/vue/Widgets/Status.vue
--- a/src/resources/vue/Widgets/Status.vue
+++ b/src/resources/vue/Widgets/Status.vue
@@ -22,7 +22,7 @@
Verify your domain to finish the setup process.
</p>
<div v-if="scope == 'domain'">
- <button id="status-verify" class="btn btn-secondary" @click="confirmDomain">
+ <button id="status-verify" class="btn btn-secondary text-nowrap" @click="confirmDomain">
<svg-icon icon="sync-alt"></svg-icon> Verify
</button>
</div>
diff --git a/src/tests/Browser/Admin/LogonTest.php b/src/tests/Browser/Admin/LogonTest.php
--- a/src/tests/Browser/Admin/LogonTest.php
+++ b/src/tests/Browser/Admin/LogonTest.php
@@ -99,7 +99,7 @@
// Click the Logout button
$browser->within(new Menu(), function ($browser) {
- $browser->click('.link-logout');
+ $browser->clickMenuItem('logout');
});
// We expect the logon page
diff --git a/src/tests/Browser/Components/Menu.php b/src/tests/Browser/Components/Menu.php
--- a/src/tests/Browser/Components/Menu.php
+++ b/src/tests/Browser/Components/Menu.php
@@ -46,12 +46,16 @@
*
* @param \Laravel\Dusk\Browser $browser
* @param array $items List of menu items
+ * @param string $active Expected active item
*
* @return void
*/
- public function assertMenuItems($browser, array $items)
+ public function assertMenuItems($browser, array $items, string $active = null)
{
- // TODO: On mobile the links will not be visible
+ // On mobile the links are not visible, show them first (wait for transition)
+ if ($browser->isPhone()) {
+ $browser->click('@toggler')->waitFor('.navbar-collapse.show');
+ }
foreach ($items as $item) {
$browser->assertVisible('.link-' . $item);
@@ -59,21 +63,36 @@
// Check number of items, to make sure there's no extra items
PHPUnit::assertCount(count($items), $browser->elements('li'));
+
+ if ($active) {
+ $browser->assertPresent(".link-{$active}.active");
+ }
+
+ if ($browser->isPhone()) {
+ $browser->click('@toggler')->waitUntilMissing('.navbar-collapse.show');
+ }
}
/**
- * Assert that specified menu item is active
+ * Click menu link.
*
- * @param \Laravel\Dusk\Browser $browser
- * @param string $item Menu item name
+ * @param \Laravel\Dusk\Browser $browser The browser object
+ * @param string $name Menu item name
*
* @return void
*/
- public function assertActiveItem($browser, string $item)
+ public function clickMenuItem($browser, string $name)
{
- // TODO: On mobile the links will not be visible
+ // On mobile the links are not visible, show them first (wait for transition)
+ if ($browser->isPhone()) {
+ $browser->click('@toggler')->waitFor('.navbar-collapse.show');
+ }
- $browser->assertVisible(".link-{$item}.active");
+ $browser->click('.link-' . $name);
+
+ if ($browser->isPhone()) {
+ $browser->waitUntilMissing('.navbar-collapse.show');
+ }
}
/**
diff --git a/src/tests/Browser/LogonTest.php b/src/tests/Browser/LogonTest.php
--- a/src/tests/Browser/LogonTest.php
+++ b/src/tests/Browser/LogonTest.php
@@ -22,10 +22,15 @@
$browser->visit(new Home())
->within(new Menu(), function ($browser) {
$browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']);
- })
- ->within(new Menu('footer'), function ($browser) {
+ });
+
+ if ($browser->isDesktop()) {
+ $browser->within(new Menu('footer'), function ($browser) {
$browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'tos', 'webmail']);
});
+ } else {
+ $browser->assertMissing('#footer-menu .navbar-nav');
+ }
});
}
@@ -73,11 +78,17 @@
$browser->on(new Dashboard())
->within(new Menu(), function ($browser) {
$browser->assertMenuItems(['support', 'contact', 'webmail', 'logout']);
- })
- ->within(new Menu('footer'), function ($browser) {
+ });
+
+ if ($browser->isDesktop()) {
+ $browser->within(new Menu('footer'), function ($browser) {
$browser->assertMenuItems(['support', 'contact', 'webmail', 'logout']);
- })
- ->assertUser('john@kolab.org');
+ });
+ } else {
+ $browser->assertMissing('#footer-menu .navbar-nav');
+ }
+
+ $browser->assertUser('john@kolab.org');
// Assert no "Account status" for this account
$browser->assertMissing('@status');
@@ -107,7 +118,7 @@
// Click the Logout button
$browser->within(new Menu(), function ($browser) {
- $browser->click('.link-logout');
+ $browser->clickMenuItem('logout');
});
// We expect the logon page
diff --git a/src/tests/Browser/SignupTest.php b/src/tests/Browser/SignupTest.php
--- a/src/tests/Browser/SignupTest.php
+++ b/src/tests/Browser/SignupTest.php
@@ -107,8 +107,7 @@
->assertMissing('@step3');
$browser->within(new Menu(), function ($browser) {
- $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login']);
- $browser->assertActiveItem('signup');
+ $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login'], 'signup');
});
$browser->waitFor('@step0 .plan-selector > .plan-box');
@@ -155,14 +154,8 @@
public function testSignupStep1(): void
{
$this->browse(function (Browser $browser) {
- $browser->visit('/signup/individual')->onWithoutAssert(new Signup());
-
- $browser->assertVisible('@step1');
-
- $browser->within(new Menu(), function ($browser) {
- $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login']);
- $browser->assertActiveItem('signup');
- });
+ $browser->visit('/signup/individual')
+ ->onWithoutAssert(new Signup());
// Here we expect two text inputs and Back and Continue buttons
$browser->with('@step1', function ($step) {
@@ -182,6 +175,10 @@
$step->assertFocused('#signup_email');
});
+ $browser->within(new Menu(), function ($browser) {
+ $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login'], 'signup');
+ });
+
// Submit invalid email, and first_name
// We expect both inputs to have is-invalid class added, with .invalid-feedback element
$browser->with('@step1', function ($step) use ($browser) {
@@ -375,7 +372,9 @@
->assertUser('signuptestdusk@' . \config('app.domain'));
// Logout the user
- $browser->click('a.link-logout');
+ $browser->within(new Menu(), function ($browser) {
+ $browser->clickMenuItem('logout');
+ });
});
}
@@ -469,7 +468,9 @@
->on(new Dashboard())
->assertUser('admin@user-domain-signup.com');
- $browser->click('a.link-logout');
+ $browser->within(new Menu(), function ($browser) {
+ $browser->clickMenuItem('logout');
+ });
});
}
@@ -523,7 +524,9 @@
->on(new Dashboard())
->assertUser('signuptestdusk@' . \config('app.domain'))
// Logout the user
- ->click('a.link-logout');
+ ->within(new Menu(), function ($browser) {
+ $browser->clickMenuItem('logout');
+ });
});
$user = $this->getTestUser('signuptestdusk@' . \config('app.domain'));
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -280,25 +280,25 @@
$browser->with('@form', function (Browser $browser) {
$browser->with('@skus', function ($browser) {
// Uncheck 'groupware', expect activesync unchecked
- $browser->click('@sku-input-groupware')
- ->assertNotChecked('@sku-input-groupware')
- ->assertNotChecked('@sku-input-activesync')
- ->assertEnabled('@sku-input-activesync')
- ->assertNotReadonly('@sku-input-activesync')
+ $browser->click('#sku-input-groupware')
+ ->assertNotChecked('#sku-input-groupware')
+ ->assertNotChecked('#sku-input-activesync')
+ ->assertEnabled('#sku-input-activesync')
+ ->assertNotReadonly('#sku-input-activesync')
// Check 'activesync', expect an alert
- ->click('@sku-input-activesync')
+ ->click('#sku-input-activesync')
->assertDialogOpened('Activesync requires Groupware Features.')
->acceptDialog()
- ->assertNotChecked('@sku-input-activesync')
+ ->assertNotChecked('#sku-input-activesync')
// Check '2FA', expect 'activesync' unchecked and readonly
- ->click('@sku-input-2fa')
- ->assertChecked('@sku-input-2fa')
- ->assertNotChecked('@sku-input-activesync')
- ->assertReadonly('@sku-input-activesync')
+ ->click('#sku-input-2fa')
+ ->assertChecked('#sku-input-2fa')
+ ->assertNotChecked('#sku-input-activesync')
+ ->assertReadonly('#sku-input-activesync')
// Uncheck '2FA'
- ->click('@sku-input-2fa')
- ->assertNotChecked('@sku-input-2fa')
- ->assertNotReadonly('@sku-input-activesync');
+ ->click('#sku-input-2fa')
+ ->assertNotChecked('#sku-input-2fa')
+ ->assertNotReadonly('#sku-input-activesync');
});
});
});
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Mon, Apr 6, 5:37 AM (14 h, 8 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18833279
Default Alt Text
D1384.1775453821.diff (29 KB)
Attached To
Mode
D1384: Mobile UI fixes
Attached
Detach File
Event Timeline